UIView: how does the appearance() proxy work? - ios

I have created a simple custom UIView:
final class TestView: UIView {
var testColor: UIColor = .white {
didSet {
backgroundColor = testColor
}
}
}
Then I wrote this in my view controller:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var testView: TestView!
#IBOutlet weak var testView2: TestView!
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
TestView.appearance().testColor = .red
}
}
}
By doing this, I get an error:
Could you help me understanding what's wrong here and how to implement the UIAppearance proxy for any custom UIView?
Thank you for your help

You need to make the property #objc and dynamic:
final class TestView: UIView {
#objc dynamic var testColor: UIColor? = .white {
didSet {
backgroundColor = testColor
}
}
}
Worth noting: the UIAppearance proxy does NOT affect views which are already part of the view hierarchy.
So, in your example, adding #objc dynamic to your property will get rid of the crash, but you will not see any change to the two #IBOutlet views.
If you call it as part of viewDidLoad() (instead of DispatchQueue.main.asyncAfter):
override func viewDidLoad() {
super.viewDidLoad()
TestView.appearance().testColor = .red
}
The two #IBOutlet views will get the red background.
Or, if you add a new view to the hierarchy, it will get the red background:
class AppearanceTestViewController: UIViewController {
#IBOutlet weak var testView: TestView!
#IBOutlet weak var testView2: TestView!
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
TestView.appearance().testColor = .red
self.addAnotherTestView()
}
}
func addAnotherTestView() -> Void {
let v = TestView()
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalToConstant: 240.0),
v.heightAnchor.constraint(equalTo: v.widthAnchor),
v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
// this newly added view WILL have a red background
}
}

Related

Strange iboutlet error for my xib custom component (unwrapping an Optional value)

Hi I m passing my viewModel from my controller to my AddToBasketView custom xib but I m not sure configure method is it true because sometimes my view appear in my viewcontroller but there was not data and also I'm getting an error unwrapping an Optional value and here is my codes;
Note: I call setupAddToBasketView func from my presenter.
AddToBasketView
final class AddToBasketView: UIView {
#IBOutlet private weak var discountedPrice: UILabel!
#IBOutlet private weak var discounteInfoStackView: UIStackView!
#IBOutlet private weak var discountedPercantage: UILabel!
#IBOutlet private weak var originalPrice: UILabel!
#IBOutlet private weak var addToFavorite: UIButton!
#IBOutlet private weak var addToFavoriteBackView: UIView!
#IBOutlet private weak var addToBasket: UIButton!
#IBOutlet private weak var addToBasketBackView: UIView!
// MARK:- Constants
private enum Constants {
enum Color {
static let greenColor = UIColor("51AB5C")
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func configureView() {
guard let view = self.loadFromNib(nibName: "AddToBasketView") else { return }
view.frame = self.bounds
self.addSubview(view)
}
func configure(with viewModel: ProductDetailViewModel?) {
guard let viewModel = viewModel else { return }
addToBasketBackView.layer.cornerRadius = 8 // Unexpectedly found nil while implicitly unwrapping an Optional value
addToFavoriteBackView.layer.cornerRadius = addToFavoriteBackView.frame.height / 2
addToBasket.backgroundColor = UIColor("\(viewModel.cart?.addToCart?.backgroundColor ?? "")")
addToBasket.setTitle(viewModel.cart?.addToCart?.text, for: .normal)
discounteInfoStackView.isHidden = viewModel.discountedPrice.1
discountedPrice.attributedText = viewModel.discountedPrice.0?.strikeThrough()
discountedPercantage.text = viewModel.discountedRate
discountedPercantage.textColor = Constants.Color.greenColor
originalPrice.textColor = (viewModel.discountedPrice.1 ? .black : Constants.Color.greenColor)
originalPrice.text = viewModel.originalPrice
}
}
ViewController
func setupAddToBasketView() {
let addToBasketView = AddToBasketView(frame: .zero)
addToBasketView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(addToBasketView)
NSLayoutConstraint.activate([
addToBasketView.heightAnchor.constraint(equalToConstant: 85),
addToBasketView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
addToBasketView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
addToBasketView.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -12)
])
addToBasketView.configure(with: self.viewModel)
}

Using UIView (Singleton) on different viewcontrollers

I have a UIView which have a button and some view to indicate sucess and failure. I am trying to use that UIView on other view controllers and receive the button action on called view controllers.
This is what i have tried so far
protocol FailViewDelegate: class {
func tryAgainTapped()
}
class AlertView: UIView {
static let instance = AlertView()
weak var delegate : FailViewDelegate?
#IBOutlet weak var titleLbl: UILabel!
#IBOutlet weak var messageLbl: UILabel!
#IBOutlet weak var dashIMageView: AnimatableImageView!
#IBOutlet weak var circleView: AnimatableView!
#IBOutlet weak var iconStatus: AnimatableImageView!
#IBOutlet weak var tryAgainButton: AnimatableButton!
#IBOutlet weak var parentView: UIView!
private override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("AlertView", owner: self, options: nil)
}
enum AlertType {
case success
case failure
}
func showAlert(alertType: AlertType, to: UIViewController) {
switch alertType {
case .success:
dashIMageView.image = UIImage(named: "circle-dash-blue")
circleView.backgroundColor = UIColor(hexString: "#4EBFFF")
titleLbl.text = "Success"
titleLbl.textColor = UIColor(hexString: "#4EBFFF")
messageLbl.text = "Your ticket has been created."
tryAgainButton.isHidden = true
iconStatus.image = UIImage(named: "icon-check")
case .failure:
dashIMageView.image = UIImage(named: "circle-dash-red")
circleView.backgroundColor = UIColor(hexString: "#EB3708")
titleLbl.text = "Failure"
titleLbl.textColor = UIColor(hexString: "#EB3708")
messageLbl.text = "There was an error, creating your ticket."
tryAgainButton.isHidden = false
iconStatus.image = UIImage(named: "icon-close")
}
parentView.center = to.view.center
to.view.addSubview(parentView)
}
func dismissAlert() {
parentView.removeFromSuperview()
}
#IBAction func tryAgainButtonTapped(_ sender: AnimatableButton) {
delegate?.tryAgainTapped()
}
}
This is how i have called the view
class CreateTicketViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
AlertView.sharedInstance.delegate = self
}
#IBAction func createTicketTapped(_ sender: AnimatableButton) {
AlertView.sharedInstance.showAlert(alertType: .failure, to: self)
}
}
extension CreateTicketViewController : FailViewDelegate {
func tryAgainTapped() {
print("Try Again Tapped")
}
}
This is the error that i got
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
(in dashIMageView.image = UIImage(named: "circle-dash-red")) and when i remove the dashImageView then the error occur for nextView and so on
You don't need to make it a singleton, which in this case (View) is a very uncommon approach, I think. You can create as many instances of that view as you want any time, anywhere (on other ViewControllers) you want and specify them, the way you want them to have.
When you want a view to be rendered and be visible, it always must be part of the view hierarchy in the current visible view controllers main view. And a view can only have ONE SUPERVIEW at the time, so whenever you add a (singleton) view to another superview, it will be removed from an other superview. If you want the same view on many view controllers (no problem), just don't let it be a singleton.
So first thing to do -> Remove the singleton design by commenting out that line:
class AlertView: UIView {
// make this line a comment or just remove it
// static let instance = AlertView()
weak var delegate : FailViewDelegate?
In your different view controllers you just create that instances of your AlertView and set the delegate correctly like this:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myAlertView = AlertView()
myAlertView.delegate = self
// then you don't need this anymore
// AlertView.sharedInstance.delegate = self
}

Interacting with custom UIView inside view controller

I am created a UIView which I want to display in my view controller. I have created the UIView and it shows with other UI components, but the problems I have now is I con not interact with the elements of the on the UIView.
below is my code
class SliderView: CustomView {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var sliderImage: UIImageView!
#IBOutlet weak var sliderText: UILabel!
override func initialize() {
super.initialize()
let name = String(describing: type(of: self))
let nib = UINib(nibName: name, bundle: .main)
nib.instantiate(withOwner: self, options: nil)
self.addSubview(self.containerView)
self.containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.containerView.topAnchor.constraint(equalTo: self.topAnchor),
self.containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return sliderImage.frame.contains(point)
}
#IBAction func clickme(_ sender: UIButton) {
print("SWIPPERD minmax22g")
}
}
in the viewcontroller
weak var sliderView: SliderView!
override func loadView() {
super.loadView()
let sliderView = SliderView()
self.view.addSubview(sliderView)
NSLayoutConstraint.activate([
sliderView.topAnchor.constraint(equalTo: self.view.topAnchor),
sliderView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
sliderView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.sliderView = sliderView
}
override func viewDidLoad() {
super.viewDidLoad()
sliderView.sliderText.text = "HOOOOO WORKS"
}
1- You shouldn't add an outlet as a subview again inside the custom view
#IBOutlet weak var containerView: UIView!
self.addSubview(self.containerView)
add this method and use it to get an instance
static func getInstance() -> SliderView {
return Bundle.main.loadNibNamed("SliderView", owner: self, options: nil)![0] as! SliderView
}
2- This will make the imageView the only active frame inside the view
return sliderImage.frame.contains(point)
3- Don 't add the subview inside loadView , add it inside viewDidLoad
let sliderView = SliderView()
to
let sliderView = SliderView.getInstance()

Removing a subview then adding it back

I have a UICollectionViewCell with a gradient subview.
In my view controller, I have buttons that are sorting CoreData and then reloadData() on the UICollectionView.
To avoid my gradient subview to be drawn again and again (as it happened in the past), I implement removeFromSuperview() in prepareForReuse().
There I also implement a flag that keeps track of gradient existence so I still add a new gradient when cell is load.
However after removing the gradient, my willMove(toSuperview: ) doesn't work, and gradient view doesn't appear.
What's wrong with my logic?
class CollectionCell: UICollectionViewCell {
#IBOutlet weak var mealImg: UIImageView!
#IBOutlet weak var mealTitleLbl: UILabel!
#IBOutlet weak var gradientView: UIView!
var gradientWasRemoved = false
func configureCell(meal: Meal) {
mealTitleLbl.text = meal.title
let img = meal.getMealImage()
mealImg.image = img
addGradient()
}
func addGradient () {
let gradient = CAGradientLayer()
gradient.frame = gradientView.bounds
let topColor = UIColor(red:0.07, green:0.07, blue:0.07, alpha:1)
let botomColor = UIColor.clear
gradient.colors = [topColor.cgColor, botomColor.cgColor]
gradientView.layer.insertSublayer(gradient, at: 0)
if gradientWasRemoved == true {
gradientView.willMove(toSuperview: self)
}
}
override func prepareForReuse() {
super.prepareForReuse()
gradientView.removeFromSuperview()
gradientWasRemoved = true
}
}
I was able to fix the logic with following.
In UICollectionViewCell class:
import UIKit
class CollectionCell: UICollectionViewCell {
#IBOutlet weak var mealImg: UIImageView!
#IBOutlet weak var gradientView: UIView!
#IBOutlet weak var mealTitleLbl: UILabel!
var gradientWasRemoved = false
func configureCell(meal: Meal) {
mealTitleLbl.text = meal.title
let img = meal.getMealImage()
mealImg.image = img
addGradient()
}
func addGradient () {
let gradient = CAGradientLayer()
gradient.frame = gradientView.bounds
let topColor = UIColor(red:0.07, green:0.07, blue:0.07, alpha:1)
let botomColor = UIColor.clear
gradient.colors = [topColor.cgColor, botomColor.cgColor]
if gradientWasRemoved == false {
gradientView.layer.insertSublayer(gradient, at: 0)
} else if gradientWasRemoved == true {
self.addSubview(gradientView)
}
}
override func prepareForReuse() {
super.prepareForReuse()
gradientView.removeFromSuperview()
gradientWasRemoved = true
}
}
In prepareForReuse( ) I didn't delete gradientView, I removed it from the Superview. I set the flag there that it was removed.
Since I didn't nil my gradientView i was able to run addGradient() and access gradientView.bounds for creation of new CGGradientLayer.
Right before adding a new layer to gradientView, I performed if check. If we did remove gradientView then we don't add a new CGGradientLayer but simply put our gradientView back.
This way we add CGGradientLayer to our gradientView only once.
I learned that by removing views from Superview they are still alive and can be edited.
Thank you #Luis and #LeoDabus for your contribution.

Implementing Protocol in Swift to Change UIView's Attribute

I thought I wrote this correctly. I'm trying to get a value set in another viewcontroller (then stored in a singleton) to change the height of the rectangle in my BarGraphView, but my BarGraphView doesn't redraw when the value I'm passing in is changed...can anyone spot what I'm missing?
BarGraphViewController:
import UIKit
class BarGraphViewController: UIViewController, BarGraphViewDataSource {
#IBOutlet weak var barGraphView: BarGraphView! {
didSet {
barGraphView.datasource = self
}
}
#IBOutlet weak var testLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
var firstBarHeight: Int = 30 {
didSet {
firstBarHeight = SharkTankSingleton.instance.firstInvestorTotalasInt / 250
updateUI()
}
}
private func updateUI() {
barGraphView.setNeedsDisplay()
}
func firstBarHeightForBarGraphView(sender: BarGraphView) -> CGFloat? {
return CGFloat(firstBarHeight)
}
BarGraphView:
import UIKit
protocol BarGraphViewDataSource: class {
func firstBarHeightForBarGraphView(sender: BarGraphView) -> CGFloat?
}
#IBDesignable
class BarGraphView: UIView {
#IBInspectable var firstBarColor: UIColor = UIColor.blueColor()
weak var datasource: BarGraphViewDataSource?
override func drawRect(rect: CGRect) {
let firstBarHeight = datasource?.firstBarHeightForBarGraphView(self) ?? 0
let firstBar = CGRect(x: 200, y: 400, width: 30, height: firstBarHeight)
// firstBarColor.setFill()
var firstBarPath = UIBezierPath(rect: firstBar)
UIColor.redColor().setFill()
firstBarPath.fill()

Resources