How does YouTube iOS app render the loading page? - ios

Just want to confirm, is the following gray section from YouTube iOS app just placeholder image? I saw a few apps make like this, if not, how do they implement that?

use this if you want to achieve it without integrating a pods.
import UIKit
#objc public protocol ListLoadable
{
func ld_visibleContentViews()->[UIView]
}
#objc extension UITableView : ListLoadable
{
public func ld_visibleContentViews()->[UIView]
{
return (self.visibleCells as NSArray).value(forKey: "contentView") as!
[UIView]
}
}
#objc extension UIView
{
public func showShimmeryLoader(){
self.isUserInteractionEnabled = false
if self is UITableView{
ListLoader.addLoaderTo(self as! UITableView)
}else if self is UICollectionView{
ListLoader.addLoaderTo(self as! UICollectionView)
}else{
ListLoader.addLoaderToViews([self])
}
}
public func hideShimmeryLoader(){
self.isUserInteractionEnabled = true
if self is UITableView{
ListLoader.removeLoaderFrom(self as! UITableView)
}else if self is UICollectionView{
ListLoader.removeLoaderFrom(self as! UICollectionView)
}else{
ListLoader.removeLoaderFromViews([self])
}
}
}
#objc extension UICollectionView : ListLoadable
{
public func ld_visibleContentViews()->[UIView]
{
return (self.visibleCells as NSArray).value(forKey: "contentView") as!
[UIView]
}
}
#objc extension UIColor {
static func backgroundFadedGrey()->UIColor
{
return UIColor(red: (246.0/255.0), green: (247.0/255.0), blue:
(248.0/255.0), alpha: 1)
}
static func gradientFirstStop()->UIColor
{
return UIColor(red: (238.0/255.0), green: (238.0/255.0), blue:
(238.0/255.0), alpha: 1.0)
}
static func gradientSecondStop()->UIColor
{
return UIColor(red: (221.0/255.0), green: (221.0/255.0), blue:(221.0/255.0)
, alpha: 1.0);
}
}
#objc extension UIView{
func boundInside(_ superView: UIView){
self.translatesAutoresizingMaskIntoConstraints = false
superView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat:
"H:|-0-[subview]-0-|", options: NSLayoutConstraint.FormatOptions(),
metrics:nil, views:["subview":self]))
superView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat:
"V:|-0-[subview]-0-|", options: NSLayoutConstraint.FormatOptions(),
metrics:nil, views:["subview":self]))
}
}
extension CGFloat
{
func doubleValue()->Double
{
return Double(self)
}
}
#objc open class ListLoader: NSObject
{
static func addLoaderToViews(_ views : [UIView])
{
CATransaction.begin()
views.forEach { $0.ld_addLoader() }
CATransaction.commit()
}
static func removeLoaderFromViews(_ views: [UIView])
{
CATransaction.begin()
views.forEach { $0.ld_removeLoader() }
CATransaction.commit()
}
public static func addLoaderTo(_ list : ListLoadable )
{
self.addLoaderToViews(list.ld_visibleContentViews())
}
public static func removeLoaderFrom(_ list : ListLoadable )
{
self.removeLoaderFromViews(list.ld_visibleContentViews())
}
}
#objc class CutoutView : UIView
{
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.rgb(red: 231, green: 235, blue: 229, alpha:
1).cgColor)
context?.fill(self.bounds)
for view in (self.superview?.subviews)! {
if view != self {
context?.setBlendMode(.clear);
let rect = view.frame
let clipPath: CGPath = UIBezierPath(roundedRect: rect,
cornerRadius: view.layer.cornerRadius).cgPath
context?.addPath(clipPath)
context?.setFillColor(UIColor.clear.cgColor)
context?.closePath()
context?.fillPath()
}
}
}
override func layoutSubviews() {
self.setNeedsDisplay()
self.superview?.ld_getGradient()?.frame = (self.superview?.bounds)!
}
}
// TODO :- Allow caller to tweak these
var cutoutHandle: UInt8 = 0
var gradientHandle: UInt8 = 0
var loaderDuration = 0.85
var gradientWidth = 0.17
var gradientFirstStop = 0.1
#objc extension UIView
{
fileprivate func ld_getCutoutView()->UIView?
{
return objc_getAssociatedObject(self, &cutoutHandle) as! UIView?
}
fileprivate func ld_setCutoutView(_ aView : UIView)
{
return objc_setAssociatedObject(self, &cutoutHandle, aView,
.OBJC_ASSOCIATION_RETAIN)
}
fileprivate func ld_getGradient()->CAGradientLayer?
{
return objc_getAssociatedObject(self, &gradientHandle) as! CAGradientLayer?
}
fileprivate func ld_setGradient(_ aLayer : CAGradientLayer)
{
return objc_setAssociatedObject(self, &gradientHandle, aLayer,
.OBJC_ASSOCIATION_RETAIN)
}
fileprivate func ld_addLoader()
{
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: self.bounds.size.width , height:
self.bounds.size.height)
self.layer.insertSublayer(gradient, at:0)
self.configureAndAddAnimationToGradient(gradient)
self.addCutoutView()
}
fileprivate func ld_removeLoader()
{
self.ld_getCutoutView()?.removeFromSuperview()
self.ld_getGradient()?.removeAllAnimations()
self.ld_getGradient()?.removeFromSuperlayer()
for view in self.subviews {
view.alpha = 1
}
}
func configureAndAddAnimationToGradient(_ gradient : CAGradientLayer)
{
gradient.startPoint = CGPoint(x: -1.0 + CGFloat(gradientWidth), y: 0)
gradient.endPoint = CGPoint(x: 1.0 + CGFloat(gradientWidth), y: 0)
gradient.colors = [
UIColor.backgroundFadedGrey().cgColor,
UIColor.gradientFirstStop().cgColor,
UIColor.gradientSecondStop().cgColor,
UIColor.gradientFirstStop().cgColor,
UIColor.backgroundFadedGrey().cgColor
]
let startLocations = [NSNumber(value: gradient.startPoint.x.doubleValue()
as Double),NSNumber(value: gradient.startPoint.x.doubleValue() as
Double),NSNumber(value: 0 as Double),NSNumber(value: gradientWidth as
Double),NSNumber(value: 1 + gradientWidth as Double)]
gradient.locations = startLocations
let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = startLocations
gradientAnimation.toValue = [NSNumber(value: 0 as Double),NSNumber(value: 1
as Double),NSNumber(value: 1 as Double),NSNumber(value: 1 +
(gradientWidth - gradientFirstStop) as Double),NSNumber(value: 1 +
gradientWidth as Double)]
gradientAnimation.repeatCount = Float.infinity
gradientAnimation.fillMode = .forwards
gradientAnimation.isRemovedOnCompletion = false
gradientAnimation.duration = loaderDuration
gradient.add(gradientAnimation ,forKey:"locations")
self.ld_setGradient(gradient)
}
fileprivate func addCutoutView()
{
let cutout = CutoutView()
cutout.frame = self.bounds
cutout.backgroundColor = UIColor.clear
self.addSubview(cutout)
cutout.setNeedsDisplay()
cutout.boundInside(self)
for view in self.subviews {
if view != cutout {
view.alpha = 0
}
}
self.ld_setCutoutView(cutout)
}
}

Related

use class in swiftui

I have these two classes which are for having the custom tapbar, I would like to use them in swiftUI how can I do? I used these wrappers but once implemented the class in the ContentView does not appear to me
I would like to do everything in swiftUI so I would prefer not to use storyboards in the implementation. it's possible ?
//SwiftUI class. I want to use this view already done in SwiftUI
HStack {
SHCircleBarControllerView()
SHCircleBarView()
}
//Class Swift 4
import UIKit
import SwiftUI
struct SHCircleBarControllerView : UIViewControllerRepresentable {
typealias UIViewControllerType = SHCircleBarController
func makeCoordinator() -> SHCircleBarControllerView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<SHCircleBarControllerView>) -> SHCircleBarController {
return SHCircleBarController()
}
func updateUIViewController(_ uiViewController: SHCircleBarController, context: UIViewControllerRepresentableContext<SHCircleBarControllerView>) {
}
class Coordinator : NSObject {
var parent : SHCircleBarControllerView
init(_ viewController : SHCircleBarControllerView){
self.parent = viewController
}
}
}
class SHCircleBarController: UITabBarController {
fileprivate var shouldSelectOnTabBar = true
private var circleView : UIView!
private var circleImageView: UIImageView!
open override var selectedViewController: UIViewController? {
willSet {
guard shouldSelectOnTabBar, let newValue = newValue else {
shouldSelectOnTabBar = true
return
}
guard let tabBar = tabBar as? SHCircleBar, let index = viewControllers?.firstIndex(of: newValue) else {return}
tabBar.select(itemAt: index, animated: true)
}
}
open override var selectedIndex: Int {
willSet {
guard shouldSelectOnTabBar else {
shouldSelectOnTabBar = true
return
}
guard let tabBar = tabBar as? SHCircleBar else {
return
}
tabBar.select(itemAt: selectedIndex, animated: true)
}
}
open override func viewDidLoad() {
super.viewDidLoad()
let tabBar = SHCircleBar()
self.setValue(tabBar, forKey: "tabBar")
self.circleView = UIView(frame: .zero)
circleView.layer.cornerRadius = 30
circleView.backgroundColor = .white
circleView.isUserInteractionEnabled = false
self.circleImageView = UIImageView(frame: .zero)
circleImageView.layer.cornerRadius = 30
circleImageView.isUserInteractionEnabled = false
circleImageView.contentMode = .center
circleView.addSubview(circleImageView)
self.view.addSubview(circleView)
let tabWidth = self.view.bounds.width / CGFloat(self.tabBar.items?.count ?? 4)
circleView.frame = CGRect(x: tabWidth / 2 - 30, y: self.tabBar.frame.origin.y - 40, width: 60, height: 60)
circleImageView.frame = self.circleView.bounds
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
circleImageView.image = image(with: self.tabBar.selectedItem?.image ?? self.tabBar.items?.first?.image, scaledTo: CGSize(width: 30, height: 30))
}
private var _barHeight: CGFloat = 74
open var barHeight: CGFloat {
get {
if #available(iOS 11.0, *) {
return _barHeight + view.safeAreaInsets.bottom
} else {
return _barHeight
}
}
set {
_barHeight = newValue
updateTabBarFrame()
}
}
private func updateTabBarFrame() {
var tabFrame = self.tabBar.frame
tabFrame.size.height = barHeight
tabFrame.origin.y = self.view.frame.size.height - barHeight
self.tabBar.frame = tabFrame
tabBar.setNeedsLayout()
}
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateTabBarFrame()
}
open override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11.0, *) {
super.viewSafeAreaInsetsDidChange()
}
updateTabBarFrame()
}
open override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let idx = tabBar.items?.firstIndex(of: item) else { return }
if idx != selectedIndex, let controller = viewControllers?[idx] {
shouldSelectOnTabBar = false
selectedIndex = idx
let tabWidth = self.view.bounds.width / CGFloat(self.tabBar.items!.count)
UIView.animate(withDuration: 0.3) {
self.circleView.frame = CGRect(x: (tabWidth * CGFloat(idx) + tabWidth / 2 - 30), y: self.tabBar.frame.origin.y - 15, width: 60, height: 60)
}
UIView.animate(withDuration: 0.15, animations: {
self.circleImageView.alpha = 0
}) { (_) in
self.circleImageView.image = self.image(with: item.image, scaledTo: CGSize(width: 30, height: 30))
UIView.animate(withDuration: 0.15, animations: {
self.circleImageView.alpha = 1
})
}
delegate?.tabBarController?(self, didSelect: controller)
}
}
private func image(with image: UIImage?, scaledTo newSize: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(newSize, _: false, _: 0.0)
image?.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height))
let newImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
You can set up your custom UIViewControllers inside the SHCircleBarController through the viewControllers property.
In SHCircleBarController
open override func viewDidLoad() {
super.viewDidLoad()
...
viewControllers = [ViewController(), ViewController2()]
}
Your other UIViewControllers
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
class ViewController2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
}
This is the result

Can't get custom activity indicator to animate

I rewrote a custom activity indicator that was originally in an Objc file into Swift. The activity indicator appears on scene but the animation isn't occurring.
I need some help figuring out why the animation isn't occurring:
vc:
class ViewController: UIViewController {
fileprivate lazy var customActivityView: CustomActivityView = {
let customActivityView = CustomActivityView()
customActivityView.translatesAutoresizingMaskIntoConstraints = false
customActivityView.delegate = self
customActivityView.numberOfCircles = 3
customActivityView.radius = 20
customActivityView.internalSpacing = 3
customActivityView.startAnimating()
return customActivityView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
}
fileprivate func setAnchors() {
view.addSubview(customActivityView)
customActivityView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
customActivityView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
customActivityView.widthAnchor.constraint(equalToConstant: 100).isActive = true
customActivityView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
}
extension ViewController: CustomActivityViewDelegate {
func activityIndicatorView(activityIndicatorView: CustomActivityView, circleBackgroundColorAtIndex index: Int) -> UIColor {
let red = CGFloat(Double((arc4random() % 256)) / 255.0)
let green = CGFloat(Double((arc4random() % 256)) / 255.0)
let blue = CGFloat(Double((arc4random() % 256)) / 255.0)
let alpha: CGFloat = 1
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
Swift file:
import UIKit
protocol CustomActivityViewDelegate: class {
func activityIndicatorView(activityIndicatorView: CustomActivityView, circleBackgroundColorAtIndex index: Int) -> UIColor
}
class CustomActivityView: UIView {
var numberOfCircles: Int = 0
var internalSpacing: CGFloat = 0
var radius: CGFloat = 0
var delay: CGFloat = 0
var duration: CFTimeInterval = 0
var defaultColor = UIColor.systemPink
var isAnimating = false
weak var delegate: CustomActivityViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
setupDefaults()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupDefaults()
fatalError("init(coder:) has not been implemented")
}
func setupDefaults() {
self.translatesAutoresizingMaskIntoConstraints = false
numberOfCircles = 5
internalSpacing = 5
radius = 10
delay = 0.2
duration = 0.8
}
func createCircleWithRadius(radius: CGFloat, color: UIColor, positionX: CGFloat) -> UIView {
let circle = UIView(frame: CGRect(x: positionX, y: 0, width: radius * 2, height: radius * 2))
circle.backgroundColor = color
circle.layer.cornerRadius = radius
circle.translatesAutoresizingMaskIntoConstraints = false;
return circle
}
func createAnimationWithDuration(duration: CFTimeInterval, delay: CGFloat) -> CABasicAnimation {
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
anim.fromValue = 0.0
anim.toValue = 1.0
anim.autoreverses = true
anim.duration = duration
anim.isRemovedOnCompletion = false
anim.beginTime = CACurrentMediaTime()+Double(delay)
anim.repeatCount = .infinity
anim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
return anim;
}
func addCircles() {
for i in 0..<numberOfCircles {
var color: UIColor?
color = delegate?.activityIndicatorView(activityIndicatorView: self, circleBackgroundColorAtIndex: i)
let circle = createCircleWithRadius(radius: radius,
color: ((color == nil) ? self.defaultColor : color)!,
positionX: CGFloat(i) * ((2 * self.radius) + self.internalSpacing))
circle.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
circle.layer.add(self.createAnimationWithDuration(duration: self.duration,
delay: CGFloat(i) * self.delay), forKey: "scale")
self.addSubview(circle)
}
}
func removeCircles() {
self.subviews.forEach({ $0.removeFromSuperview() })
}
#objc func startAnimating() {
if !isAnimating {
addCircles()
self.isHidden = false
isAnimating = true
}
}
#objc func stopAnimating() {
if isAnimating {
removeCircles()
self.isHidden = true
isAnimating = false
}
}
func intrinsicContentSize() -> CGSize {
let width: CGFloat = (CGFloat(self.numberOfCircles) * ((2 * self.radius) + self.internalSpacing)) - self.internalSpacing
let height: CGFloat = radius * 2
return CGSize(width: width, height: height)
}
func setNumberOfCircles(numberOfCircles: Int) {
self.numberOfCircles = numberOfCircles
self.invalidateIntrinsicContentSize()
}
func setRadius(radius: CGFloat) {
self.radius = radius
self.invalidateIntrinsicContentSize()
}
func setInternalSpacing(internalSpacing: CGFloat) {
self.internalSpacing = internalSpacing
self.invalidateIntrinsicContentSize()
}
}
I used the wrong key path for the animation:
I used
// incorrect
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
but I should've used
// correct
let anim: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")

CircleProgressBar around an image

I have a circular ProgressBar and I try to change to .clear the colour of a mask (CAShapeLayer) over a ImageView (under the ProgressBar) but for some reason when the progress is loading then the whole mask disappear instantly instead to vanish during the progress.
Can anyone help me to identify the bug ?
Here is my demo project: https://github.com/tygruletz/CircularProgressBar
Here is the code for my ProgressBar:
class CircularProgressBar: UIView {
var currentProgress = 0
//MARK: awakeFromNib
override func awakeFromNib() {
super.awakeFromNib()
setupView()
label.text = "\(currentProgress)"
}
//MARK: Public
public var lineWidth:CGFloat = 120 {
didSet{
foregroundLayer.lineWidth = lineWidth
backgroundLayer.lineWidth = lineWidth - (0.20 * lineWidth)
}
}
public var labelSize: CGFloat = 20 {
didSet {
label.font = UIFont.systemFont(ofSize: labelSize)
label.sizeToFit()
configLabel()
}
}
public var safePercent: Int = 100 {
didSet{
setForegroundLayerColorForSafePercent()
}
}
public func setProgress(to progressConstant: Double, withAnimation: Bool) {
var progress: Double {
get {
if progressConstant > 1 { return 1 }
else if progressConstant < 0 { return 0 }
else { return progressConstant }
}
}
foregroundLayer.strokeEnd = CGFloat(progress)
if withAnimation {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = progress
animation.duration = 2
foregroundLayer.add(animation, forKey: "foregroundAnimation")
}
var currentTime:Double = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { (timer) in
if currentTime >= 2{
timer.invalidate()
} else {
currentTime += 0.05
let percent = currentTime/2 * 100
self.currentProgress = Int(progress * percent)
self.label.text = "\(self.currentProgress)%"
self.setForegroundLayerColorForSafePercent()
self.configLabel()
}
}
timer.fire()
}
//MARK: Private
private var label = UILabel()
private let foregroundLayer = CAShapeLayer()
private let backgroundLayer = CAShapeLayer()
private var radius: CGFloat {
get{
if self.frame.width < self.frame.height { return (self.frame.width - lineWidth)/2 }
else { return (self.frame.height - lineWidth)/2 }
}
}
private var pathCenter: CGPoint{ get{ return self.convert(self.center, from:self.superview) } }
private func makeBar(){
self.layer.sublayers = nil
drawBackgroundLayer()
drawForegroundLayer()
}
private func drawBackgroundLayer(){
let path = UIBezierPath(arcCenter: pathCenter, radius: self.radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
self.backgroundLayer.path = path.cgPath
self.backgroundLayer.strokeColor = UIColor(red: CGFloat(105/255.0), green: CGFloat(105/255.0), blue: CGFloat(105/255.0), alpha: 0.85).cgColor
self.backgroundLayer.lineWidth = lineWidth - (lineWidth * 20/100)
self.backgroundLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(backgroundLayer)
}
private func drawForegroundLayer(){
let startAngle = (-CGFloat.pi/2)
let endAngle = 2 * CGFloat.pi + startAngle
let path = UIBezierPath(arcCenter: pathCenter, radius: self.radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
foregroundLayer.lineCap = CAShapeLayerLineCap.round
foregroundLayer.path = path.cgPath
foregroundLayer.lineWidth = lineWidth
foregroundLayer.fillColor = UIColor.clear.cgColor
foregroundLayer.strokeColor = UIColor.clear.cgColor
foregroundLayer.strokeEnd = 0
self.layer.addSublayer(foregroundLayer)
}
private func makeLabel(withText text: String) -> UILabel {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
label.text = text
label.font = UIFont.systemFont(ofSize: labelSize)
label.sizeToFit()
label.center = pathCenter
return label
}
private func configLabel(){
label.sizeToFit()
label.center = pathCenter
}
private func setForegroundLayerColorForSafePercent(){
let percent = currentProgress
if percent > 0 && percent < 100 {
self.backgroundLayer.strokeColor = UIColor.clear.cgColor
self.label.textColor = .orange
}
else {
self.backgroundLayer.strokeColor = UIColor(red: CGFloat(105/255.0), green: CGFloat(105/255.0), blue: CGFloat(105/255.0), alpha: 0.85).cgColor
}
}
private func setupView() {
makeBar()
self.addSubview(label)
}
//Layout Sublayers
private var layoutDone = false
override func layoutSublayers(of layer: CALayer) {
if !layoutDone {
let tempText = label.text
setupView()
label.text = tempText
layoutDone = true
}
}
}
Here I call the class in ViewController:
class ViewController: UIViewController {
//MARK: IBOutlets
#IBOutlet weak var progressBar: CircularProgressBar!
override func viewDidLoad() {
super.viewDidLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
}
#objc func handleTap() {
progressBar.labelSize = 60
progressBar.safePercent = 100
progressBar.setProgress(to: 1, withAnimation: true)
}
}
Here is a record with the bug:
Thanks for reading this.

Forwarding events with hitTest() or point(inside, with event) from view added to keyWindow

I've got a small, reusable UIView widget that can be added to any view anywhere, and may or may not always be in the same place or have the same frame. It looks something like this:
class WidgetView: UIView {
// some stuff, not very exciting
}
In my widget view, there's a situation where I need to create a popup menu with an overlay underneath it. it looks like this:
class WidgetView: UIView {
// some stuff, not very exciting
var overlay: UIView!
commonInit() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
overlay = UIView(frame: keyWindow.frame)
overlay.alpha = 0
keyWindow.addSubview(overlay)
// Set some constraints here
someControls = CustomControlsView( ... a smaller controls view ... )
overlay.addSubview(someControls)
// Set some more constraints here!
}
showOverlay() {
overlay.alpha = 1
}
hideOverlay() {
overlay.alpha = 0
}
}
Where this gets complicated, is I'm cutting the shape of the originating WidgetView out of the overlay, so that its controls are still visible underneath. This works fine:
class CutoutView: UIView {
var holes: [CGRect]?
convenience init(holes: [CGRect], backgroundColor: UIColor?) {
self.init()
self.holes = holes
self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = holes else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
... except the problem:
Touches aren't forwarded through the cutout hole. So I thought I'd be clever, and use this extension to determine whether the pixels at the touch point are transparent or not, but I can't even get that far, because hitTest() and point(inside, with event) don't respond to touches outside of the WidgetView's frame.
The way I can see it, there are four (potential) ways to solve this, but I can't get any of them working.
Find some magical (🦄) way to to make hitTest or point(inside) respond anywhere in the keyWindow, or at least the overlayView's frame
Add a UITapGestureRecognizer to the overlayView and forward the appropriate touches to the originating view controller (this partially works — the tap gesture responds, but I don't know where to go from there)
Use a delegate/protocol implementation to tell the original WidgetView to respond to touches
Add the overlay and its subviews to a different parent view altogether that isn't the keyWindow?
Below the fold, here is a complete executable setup, which relies on a new single view project with storyboard. It relies on SnapKit constraints, for which you can use the following podfile:
podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target 'YourTarget' do
pod 'SnapKit', '~> 4.2.0'
end
ViewController.swift
import UIKit
import SnapKit
class ViewController: UIViewController {
public var utilityToolbar: UtilityToolbar!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
setup()
}
func setup() {
let button1 = UtilityToolbar.Button(title: "One", buttonPressed: nil)
let button2 = UtilityToolbar.Button(title: "Two", buttonPressed: nil)
let button3 = UtilityToolbar.Button(title: "Three", buttonPressed: nil)
let button4 = UtilityToolbar.Button(title: "Four", buttonPressed: nil)
let button5 = UtilityToolbar.Button(title: "Five", buttonPressed: nil)
let menuItems: [UtilityToolbar.Button] = [button1, button2, button3, button4, button5]
menuItems.forEach({
$0.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
})
utilityToolbar = UtilityToolbar(title: "One", menuItems: menuItems)
utilityToolbar.titleButton.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
utilityToolbar.backgroundColor = .white
utilityToolbar.dropdownContainer.backgroundColor = .white
view.addSubview(utilityToolbar)
utilityToolbar.snp.makeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(250)
make.height.equalTo(50.0)
}
}
}
CutoutView.swift
import UIKit
class CutoutView: UIView {
var holes: [CGRect]?
convenience init(holes: [CGRect], backgroundColor: UIColor?) {
self.init()
self.holes = holes
self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = holes else { return }
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
UtilityToolbar.swift
import Foundation import UIKit import SnapKit
class UtilityToolbar: UIView {
class Button: UIButton {
var functionIdentifier: String?
var buttonPressed: (() -> Void)?
fileprivate var owner: UtilityToolbar?
convenience init(title: String, buttonPressed: (() -> Void)?) {
self.init(type: .custom)
self.setTitle(title, for: .normal)
self.functionIdentifier = title.lowercased()
self.buttonPressed = buttonPressed
}
}
enum MenuState {
case open
case closed
}
enum TitleStyle {
case label
case dropdown
}
private(set) public var menuState: MenuState = .closed
var itemHeight: CGFloat = 50.0
var spacing: CGFloat = 6.0 { didSet { dropdownStackView.spacing = spacing } }
var duration: TimeInterval = 0.15
var dropdownContainer: UIView!
var titleButton: UIButton = UIButton()
#IBOutlet weak fileprivate var toolbarStackView: UIStackView!
private var stackViewBottomConstraint: Constraint!
private var dropdownStackView: UIStackView!
private var overlayView: CutoutView!
private var menuItems: [Button] = []
private var expandedHeight: CGFloat { get { return CGFloat(menuItems.count - 1) * itemHeight + (spacing * 2) } }
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
convenience init(title: String, menuItems: [Button]) {
self.init()
self.titleButton.setTitle(title, for: .normal)
self.menuItems = menuItems
commonInit()
}
private func commonInit() {
self.addSubview(titleButton)
titleButton.addTarget(self, action: #selector(titleButtonPressed(_:)), for: .touchUpInside)
titleButton.snp.makeConstraints { $0.edges.equalToSuperview() }
dropdownContainer = UIView()
dropdownStackView = UIStackView()
dropdownStackView.axis = .vertical
dropdownStackView.distribution = .fillEqually
dropdownStackView.alignment = .fill
dropdownStackView.spacing = spacing
dropdownStackView.alpha = 0
dropdownStackView.translatesAutoresizingMaskIntoConstraints = true
menuItems.forEach({
$0.owner = self
$0.addTarget(self, action: #selector(menuButtonPressed(_:)), for: .touchUpInside)
})
}
override func layoutSubviews() {
super.layoutSubviews()
// Block if the view isn't fully ready, or if the containerView has already been added to the window
guard
let keyWindow = UIApplication.shared.keyWindow,
self.globalFrame != .zero,
dropdownContainer.superview == nil else { return }
overlayView = CutoutView(frame: keyWindow.frame)
overlayView.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.5)
overlayView.alpha = 0
overlayView.holes = [self.globalFrame!]
keyWindow.addSubview(overlayView)
keyWindow.addSubview(dropdownContainer)
dropdownContainer.snp.makeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset((self.globalFrame?.origin.y ?? 0) + self.frame.height)
make.height.equalTo(0)
}
dropdownContainer.addSubview(dropdownStackView)
dropdownStackView.snp.makeConstraints({ (make) in
make.left.right.equalToSuperview().inset(spacing).priority(.required)
make.top.equalToSuperview().priority(.medium)
stackViewBottomConstraint = make.bottom.equalToSuperview().priority(.medium).constraint
})
}
public func openMenu() {
titleButton.isSelected = true
dropdownStackView.addArrangedSubviews(menuItems.filter { $0.titleLabel?.text != titleButton.titleLabel?.text })
dropdownContainer.layoutIfNeeded()
dropdownContainer.snp.updateConstraints { (make) in
make.height.equalTo(self.expandedHeight)
}
stackViewBottomConstraint.update(inset: spacing)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.overlayView.alpha = 1
self.dropdownStackView.alpha = 1
self.dropdownContainer.superview?.layoutIfNeeded()
}) { (done) in
self.menuState = .open
}
}
public func closeMenu() {
titleButton.isSelected = false
dropdownContainer.snp.updateConstraints { (make) in
make.height.equalTo(0)
}
stackViewBottomConstraint.update(inset: 0)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.overlayView.alpha = 0
self.dropdownStackView.alpha = 0
self.dropdownContainer.superview?.layoutIfNeeded()
}) { (done) in
self.menuState = .closed
self.dropdownStackView.removeAllArrangedSubviews()
}
}
#objc private func titleButtonPressed(_ sender: Button) {
switch menuState {
case .open:
closeMenu()
case .closed:
openMenu()
}
}
#objc private func menuButtonPressed(_ sender: Button) {
closeMenu()
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Nothing of interest is happening here unless the touch is inside the containerView
print(UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0)
if UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0 {
return true
}
return super.point(inside: point, with: event)
} }
Extensions.swift
import UIKit
extension UIWindow {
static var topController: UIViewController? {
get {
guard var topController = UIApplication.shared.keyWindow?.rootViewController else { return nil }
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
}
}
public extension UIView {
var globalPoint: CGPoint? {
return self.superview?.convert(self.frame.origin, to: nil)
}
var globalFrame: CGRect? {
return self.superview?.convert(self.frame, to: nil)
}
}
extension UIColor {
static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {
var pixel: [CUnsignedChar] = [0, 0, 0, 0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
context!.translateBy(x: -point.x, y: -point.y)
view.layer.render(in: context!)
let red: CGFloat = CGFloat(pixel[0]) / 255.0
let green: CGFloat = CGFloat(pixel[1]) / 255.0
let blue: CGFloat = CGFloat(pixel[2]) / 255.0
let alpha: CGFloat = CGFloat(pixel[3]) / 255.0
let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)
return color
}
}
extension UIStackView {
func addArrangedSubviews(_ views: [UIView?]) {
views.filter({$0 != nil}).forEach({ self.addArrangedSubview($0!)})
}
func removeAllArrangedSubviews() {
let removedSubviews = arrangedSubviews.reduce([]) { (allSubviews, subview) -> [UIView] in
self.removeArrangedSubview(subview)
return allSubviews + [subview]
}
// Deactivate all constraints
NSLayoutConstraint.deactivate(removedSubviews.flatMap({ $0.constraints }))
// Remove the views from self
removedSubviews.forEach({ $0.removeFromSuperview() })
}
}
Silly me, I need to put the hitTest on the overlay view (CutoutView) not the calling view.
class CutoutView: UIView {
// ...
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
return super.hitTest(point, with: event)
}
}

The right placeholder of custom UITextField in Swift 3 cannot display when I add constraints to it

I'm a beginner of Swift 3 and I plan to write several Custom Control (IBDesignable) in XCode 8. When I write a custom text field, I want to add right placeholder to it. However, the right placeholder would only display successfully when this text field has no constraint. I don't know what happened to it.
I hope someone can help me fix this bug, thanks a lot.
import UIKit
#IBDesignable
class RMLDesignableUITextField: UITextField {
// MARK: PROPERTIES
#IBInspectable var insetX: CGFloat = 0
#IBInspectable var insetY: CGFloat = 0
#IBInspectable var placeholderColor: UIColor = UIColor.white {
didSet {
if let placeholder = self.placeholder {
let attributes = [NSForegroundColorAttributeName: placeholderColor]
attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes)
}
}
}
// MARK: Border
var bottomBorder = CALayer()
var rightBorder = CALayer()
var topBorder = CALayer()
var leftBorder = CALayer()
#IBInspectable var showsTopBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsBottomBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsLeftBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsRightBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear
#IBInspectable var borderWidth: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
}
}
#IBInspectable var rightPlaceholder: String = "" {
didSet {
rightPlaceholderLabel.text = rightPlaceholder
}
}
fileprivate var fakePlaceholderLabel: UILabel!
fileprivate var rightPlaceholderLabel: UILabel!
fileprivate var translateX: CGFloat!
{
get {
let attributes = [NSFontAttributeName: font!]
let rightPlaceholderTextSize = rightPlaceholderLabel.text!.size(attributes: attributes)
let rightPlaceholderTextWidth = rightPlaceholderTextSize.width
let translateX = textRect(forBounds: bounds).width - rightPlaceholderTextWidth
return translateX
}
}
// MARK: Initializers
override init(frame: CGRect) {
super.init(frame: frame)
// self.setNeedsDisplay()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
// self.setNeedsDisplay()
fakePlaceholderLabel = UILabel(frame: placeholderRect(forBounds: bounds))
fakePlaceholderLabel.font = font
fakePlaceholderLabel.text = placeholder
fakePlaceholderLabel.textColor = UIColor.lightGray
fakePlaceholderLabel.alpha = 0.0
rightPlaceholderLabel = UILabel(frame: placeholderRect(forBounds: bounds))
rightPlaceholderLabel.font = font
rightPlaceholderLabel.text = rightPlaceholder
rightPlaceholderLabel.textColor = UIColor.lightGray
rightPlaceholderLabel.alpha = 0.0
}
override func layoutSubviews() {
super.layoutSubviews()
addSubview(fakePlaceholderLabel)
addSubview(rightPlaceholderLabel)
setupSubviews()
setNeedsDisplay()
}
}
// MARK: - Lifecycle
extension RMLDesignableUITextField {
override func awakeFromNib() {
super.awakeFromNib()
}
override var intrinsicContentSize : CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: UIViewNoIntrinsicMetric)
}
override func prepareForInterfaceBuilder() {
//setupSubviews()
}
}
// MARK: - Delegate Methods
extension RMLDesignableUITextField {
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
}
// MARK: - UITextField Observing
extension RMLDesignableUITextField {
override internal func willMove(toSuperview newSuperview: UIView!) {
if newSuperview != nil {
NotificationCenter.default.addObserver(self, selector: #selector(RMLDesignableUITextField.didBeginEditing(_:)), name: NSNotification.Name.UITextFieldTextDidBeginEditing, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(RMLDesignableUITextField.didEndEditing(_:)), name: NSNotification.Name.UITextFieldTextDidEndEditing, object: self)
} else {
NotificationCenter.default.removeObserver(self)
}
}
func didBeginEditing(_ notification: Notification) {
placeholder = nil
if notification.object as! RMLDesignableUITextField === self{
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: .curveEaseIn, animations: { () -> Void in
if self.text!.isEmpty {
self.fakePlaceholderLabel.transform = self.fakePlaceholderLabel.transform.translatedBy(x: self.translateX, y: 0.0)
self.fakePlaceholderLabel.alpha = 0.0
self.rightPlaceholderLabel.transform = self.rightPlaceholderLabel.transform.translatedBy(x: self.translateX, y: 0.0)
self.rightPlaceholderLabel.alpha = 1.0
}
}, completion: nil)
}
}
func didEndEditing(_ notification: Notification) {
if notification.object as! RMLDesignableUITextField === self {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: .curveEaseIn, animations: { () -> Void in
if self.text!.isEmpty {
self.fakePlaceholderLabel.transform = self.fakePlaceholderLabel.transform.translatedBy(x: -self.translateX, y: 0.0)
self.fakePlaceholderLabel.alpha = 1.0
self.rightPlaceholderLabel.transform = self.rightPlaceholderLabel.transform.translatedBy(x: -self.translateX, y: 0.0)
self.rightPlaceholderLabel.alpha = 0.0
}
}, completion: nil)
}
}
}
// MARK: - Methods
extension RMLDesignableUITextField {
fileprivate func pnpAddBorder(_ border: CALayer, color: UIColor, frame: CGRect) {
border.backgroundColor = color.cgColor
border.frame = frame
layer.addSublayer(border)
// layer.masksToBounds = true
}
func pnpAddTopBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(topBorder, color: color, frame: CGRect(x: 0, y: 0, width: frame.width, height: width))
}
func pnpAddBottomBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(bottomBorder, color: color, frame: CGRect(x: 0, y: frame.height - width, width: frame.width, height: width))
}
func pnpAddLeftBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(leftBorder, color: color, frame: CGRect(x: 0, y: 0, width: width, height: frame.height))
}
func pnpAddRightBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(rightBorder, color: color, frame: CGRect(x: frame.width - width, y: 0, width: width, height: frame.height))
}
func setupSubviews() {
// if (showsTopBorder || showsBottomBorder || showsLeftBorder || showsRightBorder) && borderWidth == 0 {
// borderWidth = 1
// }
if showsTopBorder {
pnpAddTopBorder(borderWidth, color: borderColor)
} else {
topBorder.removeFromSuperlayer()
}
if showsBottomBorder {
pnpAddBottomBorder(borderWidth, color: borderColor)
} else {
bottomBorder.removeFromSuperlayer()
}
if showsLeftBorder {
pnpAddLeftBorder(borderWidth, color: borderColor)
} else {
leftBorder.removeFromSuperlayer()
}
if showsRightBorder {
pnpAddRightBorder(borderWidth, color: borderColor)
} else {
rightBorder.removeFromSuperlayer()
}
self.setNeedsDisplay()
}
}

Resources