I'm subclassing UISlider and overriding maximumValue to be 5, minimumValue to be 0, isContinuous to be false, and both track tint colors. Is there something I NEED to override to fix this issue, such as draw? I tried setting the slider directly to a value above 1, but it still doesn't go beyond it. Any help would be greatly appreciated.
There's no exceptions, debugging it shows the value that is sent to setValue is changing accordingly, the only issue I'm seeing is that the slider isn't updating visually.
class Slider: UISlider {
private var viewData: ViewData
// MARK: - Initialization
init(viewData: ViewData) {
self.viewData = viewData
super.init(frame: .zero)
render(for: viewData)
translatesAutoresizingMaskIntoConstraints = false
}
override public init(frame: CGRect) {
self.viewData = ViewData(orientation: .vertical, type: .discrete)
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func render(for viewData: ViewData) {
self.addTarget(self, action: #selector(sliderValueDidChange), for: .valueChanged)
}
#objc private func sliderValueDidChange(_ sender: UISlider) {
self.setValue(sender.value, animated: true)
Haptic.triggerImpact(.light)
}
// MARK: - Overrides
override var maximumValue: Float {
get {
switch viewData.type {
case .continuous: return 100
case .discrete: return 5
}
}
set { maximumValue = newValue }
}
override var minimumValue: Float {
get { return 0 }
set { minimumValue = newValue }
}
override var isContinuous: Bool {
get {
switch viewData.type {
case .continuous: return true
case .discrete: return false
}
}
set { isContinuous = newValue }
}
override var minimumTrackTintColor: UIColor? {
get { return .white }
set { minimumTrackTintColor = newValue }
}
override var maximumTrackTintColor: UIColor? {
get { return .gray }
set { minimumTrackTintColor = newValue }
}
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
var newBounds = super.trackRect(forBounds: bounds)
newBounds.size = CGSize(width: 222, height: 10)
return newBounds
}
override open func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
var newThumb = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
newThumb.size = CGSize(width: 30, height: 30)
return newThumb
}
override open func setValue(_ value: Float, animated: Bool) {
guard viewData.type == .discrete else {
super.setValue(value, animated: animated)
return
}
let roundedValue = Int(value)
super.setValue(Float(roundedValue), animated: animated)
setNeedsDisplay()
Haptic.triggerImpact(.light)
}
All of your overridden properties are causing infinite recursion in the set blocks.
Instead of overriding them just to set a specific value, update your render method to set the desired initial values.
private func render(for viewData: ViewData) {
self.addTarget(self, action: #selector(sliderValueDidChange), for: .valueChanged)
minimumValue = 0
switch viewData.type {
case .continuous: maximumValue = 100
case .discrete: maximumValue = 5
}
isContinuous = viewData.type == .continuous
minimumTrackTintColor = .white
maximumTrackTintColor = .gray
}
Besides this update, remove those overridden properties and the slider will work just fine.
Related
I have a subclass of a UIButton that takes the title label, and puts it under the button's image, as opposed to the right of the image:
final class ImageButton: UIButton {
#IBInspectable var cornerRadius: CGFloat = 8
#IBInspectable var borderColor: UIColor? = .black
private enum Constants {
static let imageSize: CGFloat = 40
static let titleHeight: CGFloat = 12
}
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.titleRect(forContentRect: contentRect)
}
else {
_ = super.titleRect(forContentRect: contentRect)
return CGRect(
x: 0,
y: contentRect.height - Constants.titleHeight,
width: contentRect.width,
height: Constants.titleHeight
)
}
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.imageRect(forContentRect: contentRect)
} else {
return CGRect(
x: contentRect.width / 2 - Constants.imageSize / 2,
y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - Constants.imageSize / 2,
width: Constants.imageSize,
height: Constants.imageSize
)
}
}
override var intrinsicContentSize: CGSize {
if #available(iOS 15, *) {
return super.intrinsicContentSize
}
else {
_ = super.intrinsicContentSize
let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
let spacing: CGFloat = 12
return CGSize(
width: max(size.width, Constants.imageSize),
height: Constants.imageSize + Constants.titleHeight + spacing
)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
if #available(iOS 15, *) {
var myConfiguration = UIButton.Configuration.plain()
myConfiguration.imagePlacement = .top
self.configuration = myConfiguration
} else {
titleLabel?.textAlignment = .center
}
}
override func draw(_ rect: CGRect) {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
layer.borderWidth = 1
layer.borderColor = borderColor?.cgColor
}
}
Trying to change the button's title does not have any effect:
myCustomButton.setTitle("Disable Box Select", for: .normal)
I tried adding:
myCustomButton.layer.setNeedsLayout()
myCustomButton.layer.setNeedsDisplay()
But, nothing seems to change the title of myCustomButton
This is not really an answer, but in response to the OP's comment...
Storyboard:
Complete controller code (using your posted ImageButton class, unedited):
class CustBtnVC: UIViewController {
#IBOutlet var myCustomButton: ImageButton!
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 15.0, *) {
var cfg = myCustomButton.configuration
cfg?.title = "Test"
myCustomButton.configuration = cfg
} else {
// Fallback on earlier versions
myCustomButton.setTitle("Pre-15 Test", for: .normal)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if #available(iOS 15.0, *) {
var cfg = myCustomButton.configuration
cfg?.title = "Disable Box Select"
myCustomButton.configuration = cfg
} else {
// Fallback on earlier versions
myCustomButton.setTitle("Pre-15 Disable Box Select", for: .normal)
}
}
}
Note: since your custom button code is setting the button style for iOS 15+ the button .configuration should be used instead of .setTitle(...) when running on iOS 15+
On launch:
after tap on the view:
I have custom UICollectionViewCell and try to round corners for button, which doesn't work.
I had the same problem for ViewController and the problem was that I was doing rounding in viewDidLoad instead of subviewsDidLoad.
I had no idea what is a problem now.
sharing my code.
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
initialsButton.layer.cornerRadius = 0.5 * initialsButton.frame.size.width
initialsButton.clipsToBounds = true
initialsButton.layer.masksToBounds = true
}
-> but I also tried without .clipsToBounds and .masksToBounds. Same result.
Here is the result. It is not a circle, it makes corner wrongly
SEE THIS RESULT
The problem was that I was doing rounding corners in awakeFromNib(), instead you MUST do it in layoutSubviews() and it works nicely.
I suppose you are using IBOutlet's
Create a class something like this, add button to your cell in storyboard, edit there how ever you want, it easier. Your code up is not good, just remove size, frame.height / 2 is what you want if you are going that way.
#IBDesignable
class RoundedBtn: UIButton {
#IBInspectable var cornerRadius: Double {
get {
return Double(self.layer.cornerRadius)
}set {
self.layer.cornerRadius = CGFloat(newValue)
}
}
#IBInspectable var borderWidth: Double {
get {
return Double(self.layer.borderWidth)
}
set {
self.layer.borderWidth = CGFloat(newValue)
}
}
#IBInspectable var borderColor: UIColor? {
get {
return UIColor(cgColor: self.layer.borderColor!)
}
set {
self.layer.borderColor = newValue?.cgColor
}
}
#IBInspectable var shadowColor: UIColor? {
get {
return UIColor(cgColor: self.layer.shadowColor!)
}
set {
self.layer.shadowColor = newValue?.cgColor
}
}
#IBInspectable var shadowOffSet: CGSize {
get {
return self.layer.shadowOffset
}
set {
self.layer.shadowOffset = newValue
}
}
#IBInspectable var shadowRadius: Double {
get {
return Double(self.layer.shadowRadius)
}set {
self.layer.shadowRadius = CGFloat(newValue)
}
}
#IBInspectable var shadowOpacity: Float {
get {
return self.layer.shadowOpacity
}
set {
self.layer.shadowOpacity = newValue
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal)
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
I've implemented popover for UIButton:
#objc func showList(sender: AnyObject?) {
guard let buttonView = sender?.value(forKey: "view") as? UIButton else { return }
guard let popVC = R.storyboard.first.VC() else { return }
popVC.modalPresentationStyle = .popover
let popOverVC = popVC.popoverPresentationController
popOverVC?.delegate = self
popOverVC?.sourceView = buttonView
popOverVC?.sourceRect = CGRect(x: 0, y: 190, width: 0, height: 0)
popOverVC?.permittedArrowDirections = .init(rawValue: 0)
popVC.preferredContentSize = CGSize(width: 150, height: 250)
showedViewController.present(popVC, animated: true, completion: nil)
}
extension VCInteractor: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
It's working good, but I need one fix - I want to hide UIVisualEffectBackdropView for my popover (it's like blur under my UITableViewController). So, I can't find any info how to disable (hide) this blur, do you have any ideas?
I've fixed my issue with my custom class
class PopoverBackgroundView: UIPopoverBackgroundView {
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.shadowColor = UIColor.clear.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override static func contentViewInsets() -> UIEdgeInsets {
return UIEdgeInsets.zero
}
override static func arrowHeight() -> CGFloat {
return 0
}
override var arrowDirection: UIPopoverArrowDirection {
get { return UIPopoverArrowDirection.down }
set {
setNeedsLayout()
}
}
override var arrowOffset: CGFloat {
get { return 0 }
set {
setNeedsLayout()
}
}
}
and I've added it like that
popOverVC?.popoverBackgroundViewClass = PopoverBackgroundView.self
that's all
here i have a very simple solution.
let appearance = UIVisualEffectView.appearance(whenContainedInInstancesOf: [NSClassFromString("_UIPopoverStandardChromeView")! as! UIAppearanceContainer.Type])
appearance.effect = UIVisualEffect()
add the code to the AppDelegate.swift file or anywhere the app is getting initialized.
I suggest adding this to the PopoverBackgroundView that Vadim Nikolaev has posted above. This removes the "clear halo" effect when removing the blur.
`override func didMoveToWindow() {
super.didMoveToWindow()
if #available(iOS 13, *) {
// iOS 13 (or newer)
if let window = UIApplication.shared.keyWindow {
let transitionViews = window.subviews.filter { String(describing: type(of: $0)) == "UITransitionView" }
for transitionView in transitionViews {
let shadowView = transitionView.subviews.filter { String(describing: type(of: $0)) == "_UICutoutShadowView" }.first
shadowView?.isHidden = true
}
}
}
}`
This is the code of creating a custom segmented control i found over the Internet. I have a problem in understanding the last two functions, beginTrackingWithTouch and layoutSubviews. What are the purpose of these functions and what does their code do exactly
and finally, excuse this question. I'm still a beginner in iOS development and I'm just seeking help.
#IBDesignable class CustomSegmentedControl : UIControl {
private var labels = [UILabel]()
var items = ["Item 1","Item 2"] {
didSet {
setUpLabels()
}
}
var thumbView = UIView()
var selectedIndex : Int = 0 {
didSet {
displayNewSelectedIndex()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView() {
//layer.cornerRadius = frame.height / 2
layer.borderColor = UIColor.blackColor().CGColor
layer.borderWidth = 2
backgroundColor = UIColor.clearColor()
setUpLabels()
insertSubview(thumbView, atIndex: 0)
}
func setUpLabels() {
for label in labels {
label.removeFromSuperview()
}
labels.removeAll(keepCapacity: true)
for index in 1...items.count {
let label = UILabel(frame: CGRectZero)
label.text = items[index-1]
label.textAlignment = .Center
label.textColor = UIColor.blackColor()
self.addSubview(label)
labels.append(label)
}
}
func displayNewSelectedIndex() {
let label = labels[selectedIndex]
self.thumbView.frame = label.frame
}
override func layoutSubviews() {
super.layoutSubviews()
var selectFrame = self.bounds
let newWidth = CGRectGetWidth(selectFrame) / CGFloat(items.count)
selectFrame.size.width = newWidth
thumbView.frame = selectFrame
thumbView.backgroundColor = UIColor.grayColor()
//thumbView.layer.cornerRadius = thumbView.frame.height / 2
let labelHeight = self.bounds.height
let labelWidth = self.bounds.width / CGFloat(labels.count)
for index in 0..<labels.count {
let label = labels[index]
let xPosition = CGFloat(index) * labelWidth
label.frame = CGRectMake(xPosition, 0, labelWidth, labelHeight)
}
}
override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
let location = touch.locationInView(self)
var calculatedIndex : Int?
for (index, item) in labels.enumerate() {
if item.frame.contains(location) {
calculatedIndex = index
}
if calculatedIndex != nil {
selectedIndex = calculatedIndex!
sendActionsForControlEvents(.ValueChanged)
}
}
return false
}
}
I can explain begin tracking method, the other one is researchable i think so
/*** beginTrackingWithTouch.. method to customize tracking. ***/
// Parameters : touch : this returns the touch that occurred at a certain point in the view. withEvent, returns the UIEvent
// associated with the touch.
// Return Type: Boolean.
override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
let location = touch.locationInView(self) // This line returns the location of touch in the view. This location is in CGPoint.
var calculatedIndex : Int?
for (index, item) in labels.enumerate() { /// enumeration of an array gives you sequence type of integer and corresponding element type of the array.
if item.frame.contains(location) { /// so labels.enumerate returns the key value pairs like so : [(0, labelatIndex0), (1, labelatIndex1).. and so on.]
calculatedIndex = index /// here index is the key, and item is the value (label.)
// So for every label/index pair, you check whether the touch happened on the label by getting the frame of the label and checking if location is a part of the frame
/// You equate the index to calculatedIndex
}
// Now you if your calculated index is a valid number, you class, which extends UIControl, will call its method stating the change of value(sendActionsForControlEvents).
// This in turn, will send a message to all the targets that have been registered using addTarget:action:forControlEvents: method.
if calculatedIndex != nil {
selectedIndex = calculatedIndex!
sendActionsForControlEvents(.ValueChanged)
}
}
return false
}
I spent days to try to solve this problem but can't find any solution (except programmaticaly):
I have 2 UIViewController where the 2nd is a UIChildViewController.
In the ChildViewController I have an IBOutlet UIView class's attribute linked to a CustomClassUIview in the storyboard.
This CustomClassUIview have methods and attribute to update the shape layer define inside this class.
The problem is when I try to access to one attribute of this custom uiview, it returns nil.
I know that the IBOutlet is fired outside viewDidLoad, viewWillAppear,... but I don't know how to keep alloc.
I did with storyboard to be easier to design but if I did this only programmaticaly it works.
Any help please.
class ChildViewController: UIViewController {
#IBOutlet weak var customUIView: CustomClassUIView!
var upperValueProgress:CGFloat = 0 {
didSet {
self.customUIView.progress = upperValueProgress
updateLayerFrames()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.greenColor()
}
override func viewWillAppear(animated: Bool) {
customUIView.progress = 0
}
func updateLayerFrames() {
customUIView.reveal()
}
}
class FirstViewController: UIViewController {
var customUIViewController:ChildViewController!
override func viewDidLoad() {
super.viewDidLoad()
self.customUIViewController = ChildViewController()
}
...
func update(){
self.customUIViewController.upperValueProgress = 56 // Example
}
,;
#IBDesignable class CustomClassUIview: UIView {
let pathLayer = CAShapeLayer()
var circleRadius: CGFloat {
get {
return self.frame.width / 2
}
set {
}
}
#IBInspectable var progress: CGFloat = 0 {
didSet {
reveal()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
reveal()
}
func configure() {
pathLayer.frame = self.bounds
pathLayer.lineWidth = 2
pathLayer.backgroundColor = UIColor.clearColor().CGColor
pathLayer.fillColor = UIColor.clearColor().CGColor
pathLayer.strokeColor = UIColor.redColor().CGColor
layer.addSublayer(segmentTimerPathLayer)
backgroundColor = UIColor.whiteColor()
progress = 0
}
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: self.bounds.minX, y: self.bounds.minY, width: 2*circleRadius, height: 2*circleRadius)
circleFrame.origin.x = 0
circleFrame.origin.y = 0
return circleFrame
}
func circlePath() -> UIBezierPath {
return UIBezierPath(ovalInRect: circleFrame())
}
override func layoutSubviews() {
super.layoutSubviews()
pathLayer.frame = bounds
pathLayer.path = circlePath().CGPath
}
func reveal() {
println(progress)
}
}