I created a UIButton subclass that looks like a checkmark.
Here is the class:
import UIKit
#IBDesignable
class CheckedButton: UIButton {
// MARK: - Properties
#IBInspectable var checked: Bool = false {
didSet {
// Toggle the check/uncheck images
updateImage()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
updateImage()
self.addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
private func updateImage() {
let image = checked ? UIImage(named: "checked") : UIImage(named: "unchecked")
self.setImage(image, for: .normal)
}
/// Called each time the button is tapped, and toggles the checked property
#objc private func tapped() {
checked = !checked
print("New value: \(checked)")
}
}
Since I set the checkedproperty as #IBInspectable, I see it in IB :
The weird thing is:
if I let this property as default, it is correctly showing in the storyboard
but if I choose either on or off inthe inspector, the screen is not updated properly.
As the class is marked #IBDesignable, I would expect the button appearance to update in IB according to the value set for this property in the inspector tab.
Got a clue?
UIimage(named:) method uses main bundle but Interface Builder load resources in different way.
Try this:
UIImage(named: "checked", in: bundle, compatibleWith: nil)
#IBDesignable
class CheckedButton: UIButton {
// MARK: - Properties
#IBInspectable var checked: Bool = false {
didSet {
// Toggle the check/uncheck images
updateImage()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setup()
}
internal func setup() {
self.addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
private func updateImage() {
let bundle = Bundle(for: CheckedButton.self)
let image = checked ? UIImage(named: "checked", in: bundle, compatibleWith:nil) : UIImage(named: "unchecked", in: bundle, compatibleWith:nil)
self.setBackgroundImage(image, for: .normal)
}
/// Called each time the button is tapped, and toggles the checked property
#objc private func tapped() {
checked = !checked
print("New value: \(checked)")
}
My suggestion is in using other custom function for getting image from assets:
extension UIImage {
public static func image(name: String, _ sender: UIView?) -> UIImage? {
#if TARGET_INTERFACE_BUILDER
if let sender = sender {
let bundle = Bundle(for: sender.classForCoder)
return UIImage(named: name, in: bundle, compatibleWith: sender.traitCollection)
} else {
return UIImage(named: name)
}
#else
return UIImage(named: name)
#endif
}
}
For your example:
private func updateImage() {
let image = checked ? UIImage.image(name: "checked", self) : UIImage(name: "unchecked", self)
setImage(image, for: .normal)
}
Also, UIButton class has checked state, which you can use without creation own sub class:
let btn = UIButton(type: .custom)
btn.setImage(UIImage.image(name: "unchecked", self), for: .normal)
btn.setImage(UIImage.image(name: "checked", self), for: .selected)
btn.isSelected = true // change button state between checked and unchecked
Related
So I'm creating an application that will need a checklist so I was able to create a custom checkbox class to make the checkbox and add that onto my uiButtons which work fine but upon opening up and building the app again it does not save state of the checkbox so how can I make it that the button input will be added to user defaults using this code so when I open the app again the checkbox input will have saved and be returned?
import UIKit
class CheckBox: UIButton {
//images
let checkedImage = UIImage(named: "checkedd_checkbox")
let unCheckedImage = UIImage(named: "uncheckedd_checkbox")
//bool propety
#IBInspectable var isChecked:Bool = false{
didSet{
self.updateImage()
}
}
override func awakeFromNib() {
self.addTarget(self, action: #selector(CheckBox.buttonClicked), for: UIControl.Event.touchUpInside)
self.imageView?.contentMode = .scaleAspectFit
self.imageEdgeInsets = UIEdgeInsets(top: 5.0, left: 0.0, bottom: 5.0, right: 50.0)
self.updateImage()
}
func updateImage() {
if isChecked == true{
self.setImage(checkedImage, for: [])
}else{
self.setImage(unCheckedImage, for: [])
}
}
#objc func buttonClicked(sender:UIButton) {
if(sender == self){
isChecked = !isChecked
}
}
}
Just set Userdefault in buttonClicked action like
#objc func buttonClicked(sender:UIButton) {
if(sender == self){
isChecked = !isChecked
UserDefaults.standard.set(isChecked, forKey: "give a spesific name here")
}
}
And check button is selected status in
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let getCheckStatus = UserDefaults.standard.bool(forKey: "given spesific name here")
self.isChecked = getCheckStatus
updateImage()
}
If you load this view from storyboard or xib use required init?(coder aDecoder: NSCoder) , If you add manually use override init(frame: CGRect) {... }
I need to create a very similar class of the RatingControl class that you make when you create the FoodTracker tutorial. Only difference with mine is that I need that class to be able to create different instances of the rating control, that vary with the types of images. So instead of just having my positivePointRatingButtons UIButton array, I need it to be able to instantiate either the negativePointRatingButtons or superPointRatingButtons as-well. I just dont want to make another whole class just for this purpose, and I am new so I figure Id get some help.
import UIKit
#IBDesignable class PointRatingControl: UIStackView {
//MARK: Properties
private var positivePointRatingButtons = [UIButton]()
private var negativePointRatingButtons = [UIButton]()
private var superPointRatingButtons = [UIButton]()
var rating = 0 {
didSet {
updatePointButtonSelectionStates()
}
}
#IBInspectable var circleType: Int = 1 {
didSet {
setupButtons()
}
}
#IBInspectable var circleSize: CGSize = CGSize(width: 30.0, height: 30.0) {
didSet {
setupButtons()
}
}
#IBInspectable var circleCount: Int = 10 {
didSet {
setupButtons()
}
}
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupButtons()
}
//MARK: Button Action
#objc func ratingButtonTapped(button: UIButton) {
guard let index = positivePointRatingButtons.index(of: button) else {
fatalError("The button, \(button), is not in the positiveRatingButtons array: \(positivePointRatingButtons)")
}
// Calculate the rating of the selected button
let selectedRating = index + 1
if selectedRating == rating {
// If the selected star represents the current rating, reset the rating to 0.
rating = 0
} else {
// Otherwise set the rating to the selected star
rating = selectedRating
}
}
//MARK: Private Methods
private func setupButtons() {
// Clear any existing buttons
for button in positivePointRatingButtons {
removeArrangedSubview(button)
button.removeFromSuperview()
}
positivePointRatingButtons.removeAll()
// Load button images, since its #IDDesignable in order to show in the interface builder you have to specifyexplicitly the catalog's bundle, as opposed to just using UIImage(named:) method
let bundle = Bundle(for: type(of: self))
let emptyCircle = UIImage(named: "greenCirclePhoto", in: bundle, compatibleWith: self.traitCollection)
let selectedCircle = UIImage(named: "greenFilledCirclePhoto", in: bundle, compatibleWith: self.traitCollection)
let highlightedCircle = UIImage(named: "greenSelectedCirclePhoto", in: bundle, compatibleWith: self.traitCollection)
for _ in 0..<circleCount {
let button = UIButton()
button.setImage(emptyCircle, for: .normal)
button.setImage(selectedCircle, for: .selected)
button.setImage(highlightedCircle, for: .highlighted)
button.setImage(highlightedCircle, for: [.highlighted, .selected])
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: circleSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: circleSize.width).isActive = true
button.addTarget(self, action: #selector(PointRatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
addArrangedSubview(button)
positivePointRatingButtons.append(button)
}
updatePointButtonSelectionStates()
}
private func updatePointButtonSelectionStates() {
for (index, button) in positivePointRatingButtons.enumerated() {
// If the index of a button is less than the rating, that button should be selected.
button.isSelected = index < rating
}
}
}
Id like to be able to use #IBInspectable aswell using the circleType property I defined so that I can use like 1, 2, 3 Integers as representations for each case.
I figured out how to do it. I just made a switch case to load up different images based on the ratingType variable
I want to make a custom button and give an input inside a selector inside the custom button class, but it give me an error massage. how can I fix it
I was test it in the playground since when I running to simulator the app is crash. this is the error show up in the playground
terminating with uncaught exception of type NSException
this is my code in playground
class customButton: UIButton {
let titleName: String
let selectorName: String
init(titleName: String, selectorName: String) {
self.titleName = titleName
self.selectorName = selectorName
super.init(frame: .zero)
self.setTitle(titleName, for: .normal)
self.addTarget(self, action: Selector(selectorName), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class viewController: UIViewController {
override func loadView() {
super.loadView()
let testButton = customButton(titleName: "Test", selectorName: "handleButton")
testButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testButton)
testButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
testButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
func handleButton() {
print("Pressed")
}
}
new problem is
Cannot convert a value type of (viewController) -> () expected argument type UIViewController
I make a custom UIButton with target selector as an input, when I try to add input in the custom button it show in Xcode
Cannot convert value of type '(ProfileController) -> () -> (ProfileController)' to expected argument type 'UIViewController
this is my code in my ProfileController and my custom button
class CustomButton: UIButton {
let imageName: UIImage
let selectorName: String
let target: UIViewController
init(imageName: UIImage, selectorName: String, target: UIViewController) {
self.imageName = imageName
self.selectorName = selectorName
self.target = target
super.init(frame: .zero)
self.setImage(imageName.withRenderingMode(.alwaysOriginal), for: .normal)
self.addTarget(target, action: Selector(selectorName), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ProfileController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let profileID = "profileID"
let profileImageView: UIImageView = {
let iv = UIImageView()
iv.image = #imageLiteral(resourceName: "581010.jpg-r_1280_720-f_jpg-q_x-xxyxx").withRenderingMode(.alwaysOriginal)
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
let pickerImage = CustomButton(imageName: #imageLiteral(resourceName: "user-6"), selectorName: "handleImagePicker", target: self) //this is the error message show up in the target: self
You should remove the selector as an initializer argument. You should set the target wherever you are creating the button. Also you need to put #objc for handleButton to be called by the Selector. Your setup will look like this,
class customButton: UIButton {
let titleName: String
init(titleName: String) {
self.titleName = titleName
super.init(frame: .zero)
self.setTitle(titleName, for: .normal)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class viewController: UIViewController {
override func loadView() {
super.loadView()
let testButton = customButton(titleName: "Test")
testButton.translatesAutoresizingMaskIntoConstraints = false
testButton.addTarget(self, action: Selector("handleButton"), for: .touchUpInside)
view.addSubview(testButton)
testButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
testButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc func handleButton() {
print("Pressed")
}
}
In latest Swift, you should change Selector("handleButton") to #selector(handleButton)
If you want to persist with current implementation then you need to take the target argument in customButton initializer as below,
init(titleName: String, selectorName: String, target: UIViewController) {
self.titleName = titleName
self.selectorName = selectorName
super.init(frame: .zero)
self.setTitle(titleName, for: .normal)
self.addTarget(target, action: Selector(selectorName), for: .touchUpInside)
}
Now you will pass the ViewController reference as this,
let testButton = customButton(titleName: "Test", selectorName: "handleButton", target: self)
And don't forget to mark the handleButton with objc as,
#objc func handleButton() {
Remove
self.addTarget(self, action: Selector(selectorName), for: .touchUpInside)
from customButton class. And Add
testButton.addTarget(self, action: Selector(handleButton), for: .touchUpInside)
in viewDidLoad() method.
Also add selector method in viewController class
#objc func handleButton() {
print("Pressed")
}
The app is crashing because in below line you have set target to self in customButton class
self.addTarget(self, action: Selector(selectorName), for: .touchUpInside)
and customButton class don' have method handleButton
handleButton is in viewcontroller so you have to set target to viewcontroller
so you have to make slight modification in your code as below
class customButton: UIButton {
let titleName: String
let selectorName: String
let ViewController:UIViewController
init(titleName: String, selectorName: String,ViewController:UIViewController) {
self.titleName = titleName
self.selectorName = selectorName
self.ViewController = ViewController
super.init(frame: .zero)
self.setTitle(titleName, for: .normal)
self.addTarget(ViewController, action: Selector(selectorName), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class viewController: UIViewController {
override func loadView() {
super.loadView()
let testButton = customButton(titleName: "Test", selectorName: "handleButton",ViewController:self)
testButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testButton)
testButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
testButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
func handleButton() {
print("Pressed")
}
}
This line will cause crash:
self.addTarget(self, action: Selector(selectorName), for: .touchUpInside)
In this line you are saying that customButton has a method named selectorName, But actually that method resides in viewController.
if you add this inside customButton class, then
class customButton: UIButton {
let titleName: String
let selectorName: String
init(titleName: String, selectorName: String) {
self.titleName = titleName
self.selectorName = selectorName
super.init(frame: .zero)
self.setTitle(titleName, for: .normal)
self.addTarget(self, action: Selector("handleTap"), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func handleButton() {
print("Pressed")
}
}
OR put add target in ViewController class like,
let testButton = customButton(titleName: "Test", selectorName: "handleButton")
testButton.addTarget(self, action: Selector("handleButton"), for: .touchUpInside)
In Swift 4.2,
Please use,
self.addTarget(self, action: #selector(handleButton), for: .touchUpInside)
And for handleButton, Obj-C runtime is required in order to look up the method implementation for a given selector,
#objc func handleButton() {
print("handle button selected")
}
In the screen shot below please note that the DropDownButton (the selected view) is not being live rendered. Also, please note the "Designables Up To Date" in the Identity Inspector. Finally, please note the two break points in the assistant editor: if I execute Editor -> Debug Selected Views then both of these break points are hit.
Here's what It looks like when I run it:
Here's the code:
#IBDesignable
class DropDownButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
if image(for: .normal) == nil {
//setImage(#imageLiteral(resourceName: "DropDown"), for: .normal)
let bundle = Bundle(for: DropDownButton.self)
if let image = UIImage(named: "DropDown", in: bundle, compatibleWith: nil) {
setImage(image, for: .normal) // Editor -> Debug Selected Views reaches this statement
}
}
if title(for: .normal) == nil {
setTitle("DropDown", for: .normal) // Editor -> Debug Selected Views reaches this statement
}
addTarget(self, action: #selector(toggle), for: .touchUpInside)
}
override func layoutSubviews() {
super.layoutSubviews()
if var imageFrame = imageView?.frame, var labelFrame = titleLabel?.frame {
labelFrame.origin.x = contentEdgeInsets.left
imageFrame.origin.x = labelFrame.origin.x + labelFrame.width + 2
imageView?.frame = imageFrame
titleLabel?.frame = labelFrame
}
}
override func setTitle(_ title: String?, for state: UIControlState) {
super.setTitle(title, for: state)
sizeToFit()
}
public var collapsed: Bool {
return imageView?.transform == CGAffineTransform.identity
}
public var expanded: Bool {
return !collapsed
}
private func collapse() {
imageView?.transform = CGAffineTransform.identity
}
private func expand() {
imageView?.transform = CGAffineTransform(rotationAngle: .pi)
}
#objc private func toggle(_: UIButton) {
if collapsed { expand() } else { collapse() }
}
}
First Edit:
I added the prepareForInterfaceBuilder method as per #DonMag's answer. Doing so made an improvement but there is still something wrong: Interface builder seems confused about the frame. When I select the button only the title is selected, not the image (i.e. triangle). I added a border; it goes around both the title and the image. Here is a picture:
If I drag the button to a new position then everything moves, title and image.
Also, it surprised me that prepareForInterfaceBuilder made a difference. My understanding of this method it that it allows me to do interface builder only setup such as providing dummy data.
Add this:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
initialize()
}
I was able to create a custom checkbox button class. However, when I attempt to place the button in my view, I don't get an image. Setting the background color simply returns a square of that size. Here is my code for adding the box:
let box:CheckBox = CheckBox()
box.frame = CGRectMake(screenSize.width/2, screenSize.height/2, 200, 200)
self.view.addSubview(box)
I created the class as follows:
class CheckBox: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
//other stuff
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Images
let checkedImage = UIImage(named: "checked_checkbox")
let uncheckedImage = UIImage(named: "checkbox_unchecked_icon")
// Bool property
var isChecked: Bool = false {
didSet{
if isChecked == true {
self.setImage(checkedImage, forState: .Normal)
} else {
self.setImage(uncheckedImage, forState: .Normal)
}
}
}
override func awakeFromNib() {
self.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
self.isChecked = false
}
func buttonClicked(sender: UIButton) {
if sender == self {
if isChecked == true {
isChecked = false
} else {
isChecked = true
}
}
}
}
Move your awakeFromNib code into your init().
awakeFromNib is only called when you're initialising from a nib/xib file.
This never gets called. You can test this by setting a breakpoint.
This is how it should look:
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
self.isChecked = false
}
This way, self.isChecked gets called, an image is set, and you'll see more than a grey box.