I've been playing around with an idea for a button which, when held, expands to reveal other buttons (like the FAB in Android). Without releasing, sliding one's finger down should highlight other buttons, similar to haptic touch's behaviour.
Here's a crude mockup I've created using Drama: https://youtu.be/Iam8Gjv3gqM.
There should also be an option to have a label horizontally next to each button, and the order of buttons should be as shown (with the selected button at the top instead of its usual position).
I already have a class for each button (seen below), but do not know how to achieve this layout/behaviour.
class iDUButton: UIButton {
init(image: UIImage? = nil) {
super.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
if let image = image {
setImage(image, for: .normal)
}
backgroundColor = .secondarySystemFill
layer.cornerRadius = 15
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
class iDUBadgeButton: iDUButton {
var badgeLabel = UILabel()
var badgeCount = 0
override init(image: UIImage? = nil) {
super.init(image: image)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
badgeLabel.textColor = .white
badgeLabel.backgroundColor = .systemRed
badgeLabel.textAlignment = .center
badgeLabel.font = .preferredFont(forTextStyle: .caption1)
badgeLabel.alpha = 0
updateBadge()
addSubview(badgeLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
badgeLabel.sizeToFit()
let height = max(18, badgeLabel.frame.height + 5.0)
let width = max(height, badgeLabel.frame.width + 10.0)
badgeLabel.frame = CGRect(x: frame.width - 5, y: -badgeLabel.frame.height / 2, width: width, height: height);
badgeLabel.layer.cornerRadius = badgeLabel.frame.height / 2;
badgeLabel.layer.masksToBounds = true;
}
func setBadgeCount(badgeCount: Int) {
self.badgeCount = badgeCount;
self.updateBadge()
}
func updateBadge() {
if badgeCount != 0 {
badgeLabel.text = "\(badgeCount)"
}
UIView.animate(withDuration: 0.25, animations: {
self.badgeLabel.alpha = self.badgeCount == 0 ? 0 : 1
})
layoutSubviews()
}
}
If anyone could help with achieving this layout, I'd be very grateful! Thanks in advance.
PS: I'm developing the UI in Swift, but will be converting it to Objective-C once complete. If you're interested, the use case is to adapt the button in the top-left of this to offer a selection of different tags.
Related
I am trying to create a snapping UISlider in Swift with a simple line at each snap, and the issue isn't the actual snapping part it is the subviews added. The subviews added look like garbage but I do not know how to make so that the subviews are equally spread out at the point where it would snap? If you look they are slightly off
protocol SnappingSliderDelegate {
func sliderChanged(_ value: Int)
}
class SnappingSlider: UISlider {
let step:CGFloat = 5
let start:CGFloat = 15
let end:CGFloat = 70
var delegate:SnappingSliderDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
override func awakeFromNib() {
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func setupView(){
//self.setThumbImage(UIImage(named: "Play_black"), for: .normal)
self.minimumValue = Float(self.start)
self.maximumValue = Float(self.end)
for i in Int(start)...Int(end) where i % 5 == 0 {
let view = UIView(frame: CGRect(x: (Int(self.frame.width) / 11) * ((i - 15) / 5), y: 0, width: 1, height: 29))
view.backgroundColor = .lightGray
self.insertSubview(view, at: 0)
}
let view = UIView(frame: CGRect(x: self.frame.width, y: 0, width: 1, height: 29))
self.insertSubview(view, at: 0)
self.addTarget(self, action: #selector(valueChanged), for: .valueChanged)
}
#objc func valueChanged(_ sender: UISlider){
let step: Float = 5
let roundedValue = round(sender.value / step) * step
self.value = roundedValue
delegate?.sliderChanged(Int(roundedValue))
}
}
I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()
I am trying to make a custom title view for my nav bar with an image an some text next to it and this is how I did it thank to https://stackoverflow.com/a/47404105 :
class CustomTitleView: UIView
{
var title_label = CustomLabel()
var left_imageView = UIImageView()
override init(frame: CGRect){
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
setup()
}
func setup(){
self.addSubview(title_label)
self.addSubview(left_imageView)
}
func loadWith(title: String, leftImage: UIImage?)
{
title_label.text = title
title_label.font = UIFont.systemFont(ofSize: FontManager.fontSize + 5)
left_imageView.image = leftImage
setupFrames()
}
func setupFrames()
{
let height: CGFloat = 44
let image_size: CGFloat = height * 0.8
left_imageView.frame = CGRect(x: 0,
y: (height - image_size) / 2,
width: (left_imageView.image == nil) ? 0 : image_size,
height: image_size)
let titleWidth: CGFloat = title_label.intrinsicContentSize.width + 10
title_label.frame = CGRect(x: left_imageView.frame.maxX + 5,
y: 0,
width: titleWidth,
height: height)
contentWidth = Int(left_imageView.frame.width)
self.frame = CGRect(x: 0, y: 0, width: CGFloat(contentWidth), height: height)
}
var contentWidth: Int = 0
override func layoutSubviews() {
super.layoutSubviews()
self.frame.size.width = CGFloat(contentWidth)
}
But the problem is that I don't want to hardcode the frame, I want to use constraints but I have no idea where to start.
First take a imageView and set it's constraints according to your requirement and then take a Label besides that imageView set it's constraints too. Do this in your navigationBar where you want to show them.
I am facing an incomprehensible problem.
I have a login UIViewController and a ProgressLoading UIVisualEffectView.
I want to print the loading when I am making an API call and waiting for response.
Here is myProgressLoading Class
import UIKit
class ProgressLoading: UIVisualEffectView {
var text: String? {
didSet {
label.text = text
}
}
let activityIndictor: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
let label: UILabel = UILabel()
let blurEffect = UIBlurEffect(style: .light)
let vibrancyView: UIVisualEffectView
init(text: String) {
self.text = text
self.vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
self.vibrancyView.backgroundColor = UIColor(white: 0.2, alpha: 0.7)
super.init(effect: blurEffect)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
self.text = ""
self.vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
self.vibrancyView.backgroundColor = UIColor(white: 0.2, alpha: 0.7)
super.init(coder: aDecoder)
self.setup()
}
func setup() {
contentView.addSubview(vibrancyView)
contentView.addSubview(activityIndictor)
contentView.addSubview(label)
activityIndictor.startAnimating()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superview = self.superview {
let width = superview.frame.size.width / 2.3
let height: CGFloat = 50.0
self.frame = CGRect(x: superview.frame.size.width / 2 - width / 2,
y: superview.frame.height / 2 - height / 2,
width: width,
height: height)
vibrancyView.frame = self.bounds
let activityIndicatorSize: CGFloat = 40
activityIndictor.frame = CGRect(x: 5,
y: height / 2 - activityIndicatorSize / 2,
width: activityIndicatorSize,
height: activityIndicatorSize)
activityIndictor.color = UIColor.white
layer.cornerRadius = 8.0
layer.masksToBounds = true
label.text = text
label.textAlignment = NSTextAlignment.center
label.frame = CGRect(x: activityIndicatorSize + 5,
y: 0,
width: width - activityIndicatorSize - 15,
height: height)
label.textColor = UIColor.white
label.font = UIFont.boldSystemFont(ofSize: 16)
}
}
func show() {
self.isHidden = false
}
func hide() {
self.isHidden = true
}
}
Here is how I work with my progressLoading, a show and hide methods and declare with a text.
override func viewDidLoad() {
super.viewDidLoad()
progressLoading = ProgressLoading(text: "Loggin in...")
progressLoading?.hide()
self.view.addSubview(progressLoading!)
}
func startAnimatingLoading(viewModel: Login.ViewModel) {
self.progressLoading?.show()
}
func stopAnimatingLoading(viewModel: Login.ViewModel) {
self.progressLoading?.hide()
}
My problem is that when I wait for the response, I show the loading programatically, but nothing appears in my device. (if you wonder, I simulate long API callback by just making a breakpoint and stay on the breakpoint)
I looked inside the viewHierarchy and the Loading is right here in front of everything exactly how I want it to be.
Here is my ViewHierarchy :
Is there something I didn't get with the viewHierarchy ?
How is this possible that something is shown on the view hierarchy but not inside my device ?
Thanks you for your help, I just don't get it !
Are you making the API call on the main thread? If you start the animation and then block the main thread the progress indicator won't appear - the system needs at least one draw cycle to actually display the indicator. Move your simulated API call to a background thread and then show your indicator before the call and hide it when API responds. Hope this helps!
I have a view A that handles UITapGestureRecogniser. When it's on its own everything works great.
I have another View B that holds six of the View A objects. When I add View B to my ViewController the UITapGestureRecogniser stops working.
I have isUserInteractionEnabled = true on all the views.
Can anyone spot why it's not working?
How can I check if the tapGestures are being activated upon touch?
Thanks
Note: The ViewController doesn't have any UIGestureRecognisers on it
SingleView
class QTSingleSelectionView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
fileprivate func initialize() {
let tap = UITapGestureRecognizer(target: self, action: #selector(onTapListener))
addGestureRecognizer(tap)
}
func onTapListener() {
print("tap")
_isSelected.toggle()
setSelection(isSelected: _isSelected, animated: true)
}
}
Multiple views
class QTMutipleChoiceQuestionSelector: UIView {
var selectors: [QTSingleSelectionView] = []
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
fileprivate func initialize() {
for selector in selectors {
selector.removeFromSuperview()
}
selectors.removeAll()
guard let dataSource = dataSource else { return }
let count = dataSource.numberOfAnswers(self)
for index in 0..<count {
let selector = QTSingleSelectionView()
selector.frame = CGRect(x: 0, y: topMargin + heightOfSelector*CGFloat(index), width: frame.width, height: heightOfSelector)
selector.text = dataSource.mutipleChoiceQuestionSelector(self, textForAnswerAtIndex: index)
selector.selected = dataSource.mutipleChoiceQuestionSelector(self, isItemSelectedAtIndex: index)
selector.isUserInteractionEnabled = true
addSubview(selector)
}
}
}
ViewController
lazy var multipleChoiceView: QTMutipleChoiceQuestionSelector = {
let selector = QTMutipleChoiceQuestionSelector()
selector.dataSource = self
selector.isUserInteractionEnabled = true
return selector
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(multipleChoiceView)
}
I have tested your code. The problem is in line:
let selector = QTMutipleChoiceQuestionSelector() //Wrong!
This line initializes the view with frame (0,0,0,0), that means it cannot receive any touch event, even if you could see it.
The solution is giving the view a size to receive events:
let selector = QTMutipleChoiceQuestionSelector(frame: CGRect(x: 0.0, y: 0.0, width: self.view.frame.width, height: self.view.frame.height))
Try This -
Instead of :
for index in 0..<count {
let selector = QTSingleSelectionView()
selector.frame = CGRect(x: 0, y: topMargin + heightOfSelector*CGFloat(index), width: frame.width, height: heightOfSelector)
selector.text = dataSource.mutipleChoiceQuestionSelector(self, textForAnswerAtIndex: index)
selector.selected = dataSource.mutipleChoiceQuestionSelector(self, isItemSelectedAtIndex: index)
selector.isUserInteractionEnabled = true
addSubview(selector)
}
You should use :
for index in 0..<count {
let aFrame: CGRect = CGRect(x: 0, y: topMargin + heightOfSelector*CGFloat(index), width: frame.width, height: heightOfSelector)
let selector = QTSingleSelectionView(frame: aFrame)
selector.text = dataSource.mutipleChoiceQuestionSelector(self, textForAnswerAtIndex: index)
selector.selected = dataSource.mutipleChoiceQuestionSelector(self, isItemSelectedAtIndex: index)
selector.isUserInteractionEnabled = true
addSubview(selector)
}
This will call the appropriate initializer which is init(frame: CGRect) which has the initializer() inside it. But right now your code call's default init() function of the UIView which does not have your custom initialize.