CIHeightFieldFromMask output image is the same as input - ios

I've been using various CIFilters to achieve a certain effect, but when narrowing down my problem I've found that CIHeightFieldFromMask yields no result; meaning the mask image looks exactly the same after I apply the filter. I'm using the same exact black and white text image used in the Apple Docs.
ciImage = ciImage
.applyingFilter("CIHeightFieldFromMask", parameters: [kCIInputImageKey: ciImage,
kCIInputRadiusKey: ShadedHeightMaskInputRadius])
ShadedHeightMaskInputRadius is a value that I can change ranging from 0 to 100 with a slider, so I've also tried all kinds of input radii with no difference. How can I achieve the same exact result shown in the documentation?

Not sure why your attempt is failing...
This might work for you:
extension UIImage {
func applyHeightMap(withRadius: Int) -> UIImage {
guard let thisCI = CIImage(image: self) else { return self }
let newCI = thisCI
.applyingFilter("CIHeightFieldFromMask",
parameters: [kCIInputRadiusKey: withRadius])
return UIImage(ciImage: newCI)
}
}
Usage would be:
let newImg = self.maskImg.applyHeightMap(withRadius: self.radius)
Here is some sample code to try it out. It has a 0 to 50 range slider for the radius. This is (as you probably know) very slow on a simulator, so this updates the image only when you release the slider:
class CITestViewController: UIViewController {
let theSlider: UISlider = {
let v = UISlider()
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.text = " "
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleAspectFit
v.backgroundColor = .blue
return v
}()
let theSpinner: UIActivityIndicatorView = {
let v = UIActivityIndicatorView()
return v
}()
var maskImg: UIImage!
var radius: Int = 10
override func viewDidLoad() {
super.viewDidLoad()
guard let mImg = UIImage(named: "ci_heightmask") else {
fatalError("could not load ci_heightmask image")
}
maskImg = mImg
[theSlider, theLabel, theImageView, theSpinner].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
theSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
theSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
theSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
theLabel.topAnchor.constraint(equalTo: theSlider.bottomAnchor, constant: 20.0),
theLabel.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor, constant: 0.0),
theImageView.topAnchor.constraint(equalTo: theLabel.bottomAnchor, constant: 20.0),
theImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
theImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor),
theSpinner.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor),
theSpinner.centerYAnchor.constraint(equalTo: theSlider.centerYAnchor),
])
theSlider.minimumValue = 0
theSlider.maximumValue = 1
theSlider.value = Float(radius) / 50.0
theSlider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
theSlider.addTarget(self, action: #selector(sliderReleased(_:)), for: .touchUpInside)
theLabel.text = "Radius: \(radius)"
updateImage()
}
#objc func sliderChanged(_ sender: Any) {
if let s = sender as? UISlider {
let v = Int((s.value * 50).rounded())
if radius != v {
radius = v
theLabel.text = "Radius: \(radius)"
}
}
}
#objc func sliderReleased(_ sender: Any) {
updateImage()
}
func updateImage() -> Void {
theSlider.isHidden = true
theSpinner.startAnimating()
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
let newImg = self.maskImg.applyHeightMap(withRadius: self.radius)
DispatchQueue.main.async {
self.theImageView.image = newImg
self.theSpinner.stopAnimating()
self.theSlider.isHidden = false
}
}
}
}
ci_heightmask.png
Results:

Related

setting adjustsImageWhenHighlighted with UIButtonConfiguration

Since adjustsImageWhenHighlighted is deprecated in iOS 15.0, what's the way to set it with UIButtonConfiguration? If I set it directly, I get a message saying "You may customize to replicate this behavior via a configurationUpdateHandler", but what exactly should I be doing to set this in configurationUpdateHandler? Thanks
This is the basics...
Assuming you have "normal" and "highlighted" images:
// let's use new UIButton.Configuration
newButton.configuration = .plain()
newButton.configurationUpdateHandler = { button in
var config = button.configuration
config?.image = button.isHighlighted ? imgHighlighted : imgNormal
button.configuration = config
}
Here's a runnable example, that uses UIGraphicsImageRenderer to create a "darkened" version of the button image "on-the-fly":
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let imgNormal = UIImage(named: "myButtonImage")
else {
print("Could not load images!")
return
}
// generate a "darkened" version of the image
let rndr = UIGraphicsImageRenderer(size: imgNormal.size)
let imgHighlighted = rndr.image { (ctx) in
imgNormal.draw(at: .zero)
UIColor.black.withAlphaComponent(0.5).setFill()
ctx.cgContext.fill(CGRect(origin: .zero, size: imgNormal.size))
}
let oldButton = UIButton()
let newButton = UIButton()
// "standard" button
oldButton.setImage(imgNormal, for: [])
// default is .adjustsImageWhenHighlighted = true
// let's use new UIButton.Configuration
newButton.configuration = .plain()
newButton.configurationUpdateHandler = { button in
var config = button.configuration
config?.image = button.isHighlighted ? imgHighlighted : imgNormal
button.configuration = config
}
[oldButton, newButton].forEach { b in
b.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(b)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
oldButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
oldButton.widthAnchor.constraint(equalToConstant: 80.0),
oldButton.heightAnchor.constraint(equalTo: oldButton.widthAnchor),
oldButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
newButton.topAnchor.constraint(equalTo: oldButton.bottomAnchor, constant: 20.0),
newButton.widthAnchor.constraint(equalTo: oldButton.widthAnchor),
newButton.heightAnchor.constraint(equalTo: oldButton.widthAnchor),
newButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
}
}
When you run this, the two buttons should look and function the same.

Trying to update local json file

To start off, I want to say that I am new to using JSON, so if this code needs some cleanup also, let me know. So I am trying to update json file. I have created an empty jsonfile named data.json in my project, inside a folder I named Data. The file looks like this:
{
"fullname": "Testuser2",
"answered": 0,
"correct": 0
}
This is my JSONData.swift:
import Foundation
struct UserData: Codable {
let fullname: String
let answered: Int
let correct: Int
}
struct GlobalVariable{
static var fullname = String()
static var answered = Int()
static var correct = Int()
}
These function to read the json file:
private func readLocalFile(forName name: String) -> Data? {
do {
if let bundlePath = Bundle.main.path(forResource: name,
ofType: "json"),
let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
return jsonData
}
} catch {
print(error)
}
return nil
}
private func parse(jsonData: Data) {
do {
let decodedData = try JSONDecoder().decode(UserData.self,
from: jsonData)
GlobalVariable.fullname = decodedData.fullname
GlobalVariable.answered = decodedData.answered
GlobalVariable.correct = decodedData.correct
} catch {
print("decode error")
}
}
And this is how I use it:
func getJSON() {
if let localData = self.readLocalFile(forName: "data") {
self.parse(jsonData: localData)
print(GlobalVariable.fullname)
print(GlobalVariable.answered)
print(GlobalVariable.correct)
}
}
So the getJSON successfully shows me the correct values.
Now after the GlobalVariable.answered and GlobalVariable.correct has been incrementet, I want to save it.
This is the code I am trying to use to save it:
func saveJSON {
print("---------")
print(GlobalVariable.fullname)
print(GlobalVariable.answered)
print(GlobalVariable.correct)
print("---------")
let dataToUpdate = [UserData(fullname: GlobalVariable.fullname, answered: GlobalVariable.answered, correct: GlobalVariable.correct)]
do {
let jsonData = try JSONEncoder().encode(dataToUpdate)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("data.json")
do {
try jsonString.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
} catch {
// Handle error
}
}
} catch { print(error) }
}
The three print codes inside saveJSON shows the correct value, but someone nothing gets saved to the json file. The json file stays untouched.. How come?
I've also tried removing the .json from let pathWithFilename = documentDirectory.appendingPathComponent("data.json"), but still not working..
You are starting with your data.json in your bundle.
You read that, change the values, and then write it to Documents folder.
So, when you want to read the file, you need to first see if it exists in the Documents folder. If so, read it from there. If it doesn't exist (e.g. first time the app is run), then read it from the bundle.
Edit
The general idea is:
try to load the json file from the app's Documents folder
if that fails
create a "blank" user data object and show the Name Input view
if we successfully loaded the json file
use that data
after every change (name was entered, user answered a question), update the user data object and save (overwrite) the json file in the Documents folder
No need to have a .json file in your bundle, because you'll create a blank user data object if there is no .json file in the Documents folder.
Here is a complete example you can run and inspect:
struct UserData: Codable {
var fullname: String
var answered: Int
var correct: Int
}
class FileTestVC: UIViewController {
var userData: UserData!
let nameTextField = UITextField()
let nameLabel = UILabel()
let questionLabel = UILabel()
let resultNameLabel = UILabel()
let resultLabel = UILabel()
let userNameView = UIView()
let questionView = UIView()
let restartView = UIView()
let numQuestions: Int = 10
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupNameView()
setupQuestionView()
setupRestartView()
[userNameView, questionView, restartView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.layer.cornerRadius = 16
v.layer.borderWidth = 2
v.isHidden = true
view.addSubview(v)
}
userNameView.layer.borderColor = UIColor.systemBlue.cgColor
questionView.layer.borderColor = UIColor.systemRed.cgColor
restartView.layer.borderColor = UIColor.systemGreen.cgColor
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
userNameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
userNameView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
questionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
questionView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
restartView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
restartView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
userData = UserData(fullname: "", answered: 0, correct: 0)
if let jData = readLocalFile(forName: "data.json") {
do {
userData = try JSONDecoder().decode(UserData.self, from: jData)
} catch {
print(error)
}
}
if userData.fullname.isEmpty {
userNameView.isHidden = false
} else {
if userData.answered < numQuestions {
questionView.isHidden = false
nameLabel.text = userData.fullname
showNextQuestion()
} else {
showResult()
}
}
}
func setupNameView() {
// add a text field and "Save" button to userNameView
nameTextField.borderStyle = .roundedRect
let saveBtn = UIButton()
saveBtn.setTitle("Save", for: [])
saveBtn.setTitleColor(.white, for: .normal)
saveBtn.setTitleColor(.lightGray, for: .highlighted)
saveBtn.backgroundColor = .systemBlue
saveBtn.layer.cornerRadius = 8
[nameTextField, saveBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
userNameView.addSubview(v)
}
let g = userNameView
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
nameTextField.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
nameTextField.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
nameTextField.widthAnchor.constraint(equalToConstant: 280.0),
saveBtn.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20.0),
saveBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
saveBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
saveBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
saveBtn.addTarget(self, action: #selector(saveNameTapped(_:)), for: .touchUpInside)
}
func setupQuestionView() {
// add labels + Right and Wrong buttons to questionView
nameLabel.textAlignment = .center
nameLabel.backgroundColor = .yellow
nameLabel.text = "Name"
questionLabel.textAlignment = .center
questionLabel.backgroundColor = .green
questionLabel.numberOfLines = 0
questionLabel.text = "Question"
let rightBtn = UIButton()
rightBtn.setTitle("Right", for: [])
rightBtn.setTitleColor(.white, for: .normal)
rightBtn.setTitleColor(.lightGray, for: .highlighted)
rightBtn.backgroundColor = .systemGreen
rightBtn.layer.cornerRadius = 8
let wrongBtn = UIButton()
wrongBtn.setTitle("Wrong", for: [])
wrongBtn.setTitleColor(.white, for: .normal)
wrongBtn.setTitleColor(.lightGray, for: .highlighted)
wrongBtn.backgroundColor = .systemRed
wrongBtn.layer.cornerRadius = 8
[nameLabel, questionLabel, rightBtn, wrongBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
questionView.addSubview(v)
}
let g = questionView
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
nameLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
nameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
questionLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20.0),
questionLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
questionLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
questionLabel.widthAnchor.constraint(equalToConstant: 280.0),
rightBtn.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20.0),
rightBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
wrongBtn.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20.0),
wrongBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
wrongBtn.leadingAnchor.constraint(equalTo: rightBtn.trailingAnchor, constant: 20.0),
wrongBtn.widthAnchor.constraint(equalTo: rightBtn.widthAnchor),
wrongBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
rightBtn.addTarget(self, action: #selector(rightAnswerTapped(_:)), for: .touchUpInside)
wrongBtn.addTarget(self, action: #selector(wrongAnswerTapped(_:)), for: .touchUpInside)
}
func setupRestartView() {
// add labels + button to restartView
resultNameLabel.textAlignment = .center
resultNameLabel.backgroundColor = .yellow
resultNameLabel.text = "name"
resultLabel.textAlignment = .center
resultLabel.backgroundColor = .green
resultLabel.numberOfLines = 0
resultLabel.text = "You answered"
let restartBtn = UIButton()
restartBtn.setTitle("Restart", for: [])
restartBtn.setTitleColor(.white, for: .normal)
restartBtn.setTitleColor(.lightGray, for: .highlighted)
restartBtn.backgroundColor = .systemBlue
restartBtn.layer.cornerRadius = 8
[resultNameLabel, resultLabel, restartBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
restartView.addSubview(v)
}
let g = restartView
NSLayoutConstraint.activate([
resultNameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
resultNameLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultNameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
resultLabel.topAnchor.constraint(equalTo: resultNameLabel.bottomAnchor, constant: 20.0),
resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
resultLabel.widthAnchor.constraint(equalToConstant: 280.0),
restartBtn.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: 20.0),
restartBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
restartBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
restartBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
restartBtn.addTarget(self, action: #selector(restartTapped(_:)), for: .touchUpInside)
}
#objc func saveNameTapped(_ sender: Any?) {
guard let s = nameTextField.text,
!s.isEmpty
else { return }
view.endEditing(true)
userNameView.isHidden = true
questionView.isHidden = false
userData.fullname = s
nameLabel.text = userData.fullname
saveJSON(userData)
questionLabel.text = "Question \(userData.answered + 1) of \(numQuestions)\nAnswer Right or Wrong"
}
#objc func restartTapped(_ sender: Any?) {
// "empty" user data object
userData = UserData(fullname: "", answered: 0, correct: 0)
// save (overwrite) existing data
saveJSON(userData)
restartView.isHidden = true
nameTextField.text = ""
userNameView.isHidden = false
}
#objc func rightAnswerTapped(_ sender: Any?) {
userData.correct += 1
showNextQuestion()
}
#objc func wrongAnswerTapped(_ sender: Any?) {
showNextQuestion()
}
func showNextQuestion() {
userData.answered += 1
saveJSON(userData)
if userData.answered < numQuestions {
questionLabel.text = "Question \(userData.answered + 1) of \(numQuestions)\nAnswer Right or Wrong"
} else {
questionView.isHidden = true
showResult()
}
}
func showResult() {
restartView.isHidden = false
resultNameLabel.text = userData.fullname
resultLabel.text = "You answered \(userData.correct) Right!"
}
private func readLocalFile(forName name: String) -> Data? {
if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent(name)
do {
let jd = try String(contentsOf: pathWithFilename)
return jd.data(using: .utf8)
} catch {
print(error)
}
return nil
}
return nil
}
func saveJSON(_ d: UserData) {
do {
let jsonData = try JSONEncoder().encode(d)
let jsonString = String(data: jsonData, encoding: .utf8)!
if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("data.json")
do {
try jsonString.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
} catch {
print(error)
}
}
} catch {
print(error)
}
}
}

Portion of UIlabel clipped

Hi,
I have collection view cell and it appears that some portion of label is clipped. Here is my code:
class FeedbackTagCell: BaseCollectionCell {
override func prepare() {
super.prepare()
setup()
setConstraints()
}
private let lbl: UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.numberOfLines = 1
return lbl
}()
private let icon: UIImageView = {
let imgView = UIImageView()
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.contentMode = .scaleAspectFit
return imgView
}()
private func setup() {
contentView.layer.cornerRadius = 4.0
contentView.addSubview(lbl)
contentView.addSubview(icon)
}
private func setConstraints() {
let iconLeftOffset: CGFloat = 8
let iconRightOffset: CGFloat = 15
let lblVerticalOffset: CGFloat = 8
let lblLeftOffset: CGFloat = 16
let iconHeightWidth: CGFloat = 10
NSLayoutConstraint.activate([
lbl.leftAnchor.constraint(equalTo: leftAnchor, constant: lblLeftOffset),
lbl.rightAnchor.constraint(equalTo: icon.leftAnchor, constant: -iconLeftOffset),
lbl.topAnchor.constraint(equalTo: contentView.topAnchor, constant: lblVerticalOffset),
lbl.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -lblVerticalOffset),
icon.rightAnchor.constraint(equalTo: rightAnchor, constant: -iconRightOffset),
icon.centerYAnchor.constraint(equalTo: centerYAnchor),
icon.heightAnchor.constraint(equalToConstant: iconHeightWidth),
icon.widthAnchor.constraint(equalToConstant: iconHeightWidth)
])
}
}

Open DatePicker popup directly on button tap

Trying to open the Date picker popup on button tap but it is showing a label instead of picker, and on tapping that label the picker opens. Below is code:
#IBAction func changeMonth(_ sender: Any) {
let picker : UIDatePicker = UIDatePicker()
picker.datePickerMode = UIDatePicker.Mode.date
picker.addTarget(self, action: #selector(dueDateChanged(sender:)), for: UIControl.Event.valueChanged)
let pickerSize : CGSize = picker.sizeThatFits(CGSize.zero)
picker.frame = CGRect(x:0.0, y:250, width:pickerSize.width, height:460)
self.view.addSubview(picker)
}
#objc func dueDateChanged(sender:UIDatePicker){
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .none
btnMonth.setTitle(dateFormatter.string(from: sender.date), for: .normal)
}
Need this to open on button tap:
But this is being shown:
and on tapping of this date label, picker opens. I am not getting why picker not opening directly. Please guide what's wrong in above code.
Here is a very simple example of embedding the date picker in a view, then showing / hiding that view on a button tap:
class MyDatePicker: UIView {
var changeClosure: ((Date)->())?
var dismissClosure: (()->())?
let dPicker: UIDatePicker = {
let v = UIDatePicker()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let blurEffect = UIBlurEffect(style: .dark)
let blurredEffectView = UIVisualEffectView(effect: blurEffect)
let pickerHolderView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 8
return v
}()
[blurredEffectView, pickerHolderView, dPicker].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
addSubview(blurredEffectView)
pickerHolderView.addSubview(dPicker)
addSubview(pickerHolderView)
NSLayoutConstraint.activate([
blurredEffectView.topAnchor.constraint(equalTo: topAnchor),
blurredEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurredEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurredEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
pickerHolderView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
pickerHolderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
pickerHolderView.centerYAnchor.constraint(equalTo: centerYAnchor),
dPicker.topAnchor.constraint(equalTo: pickerHolderView.topAnchor, constant: 20.0),
dPicker.leadingAnchor.constraint(equalTo: pickerHolderView.leadingAnchor, constant: 20.0),
dPicker.trailingAnchor.constraint(equalTo: pickerHolderView.trailingAnchor, constant: -20.0),
dPicker.bottomAnchor.constraint(equalTo: pickerHolderView.bottomAnchor, constant: -20.0),
])
if #available(iOS 14.0, *) {
dPicker.preferredDatePickerStyle = .inline
} else {
// use default
}
dPicker.addTarget(self, action: #selector(didChangeDate(_:)), for: .valueChanged)
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
blurredEffectView.addGestureRecognizer(t)
}
#objc func tapHandler(_ g: UITapGestureRecognizer) -> Void {
dismissClosure?()
}
#objc func didChangeDate(_ sender: UIDatePicker) -> Void {
changeClosure?(sender.date)
}
}
class ViewController: UIViewController {
let myPicker: MyDatePicker = {
let v = MyDatePicker()
return v
}()
let myButton: UIButton = {
let v = UIButton()
v.setTitle("Show Picker", for: [])
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
[myButton, myPicker].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// custom picker view should cover the whole view
myPicker.topAnchor.constraint(equalTo: g.topAnchor),
myPicker.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myPicker.trailingAnchor.constraint(equalTo: g.trailingAnchor),
myPicker.bottomAnchor.constraint(equalTo: g.bottomAnchor),
myButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
myButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myButton.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
])
// hide custom picker view
myPicker.isHidden = true
// add closures to custom picker view
myPicker.dismissClosure = { [weak self] in
guard let self = self else {
return
}
self.myPicker.isHidden = true
}
myPicker.changeClosure = { [weak self] val in
guard let self = self else {
return
}
print(val)
// do something with the selected date
}
// add button action
myButton.addTarget(self, action: #selector(tap(_:)), for: .touchUpInside)
}
#objc func tap(_ sender: Any) {
myPicker.isHidden = false
}
}
Here's how it looks on start:
tapping the button will show the custom view:
and here's how it looks on iOS 13 (prior to the new UI):

iOS UIButtons in StackView aren't being tapped

I have buttons inside a ButtonView class, to add some background and a label. These ButtonViews are added to a UIStackView which is a view in the PlayOverlay Class. PlayOverlay serves as a parent class to different kinds of overlays, in this example I have only included the BeginOverlay.
BeginOverlay is presented by the PlaySecVC. The Buttons in the BeginOverlay can't be tapped for some reason. I have tried the UIDebugging in XCode to see if there are any views in front of them, and there aren't. They are the frontmost views. I do get one error When UIDebugging that tells me that ButtonView's width, height, and x and y are ambiguous. This is because i have no constraints on it, as shown below, since they are laid out the stack view. How can I make these buttons tappable?
ViewController:
import UIKit
fileprivate struct scvc {
static let overlayWidth: CGFloat = 330
static let overlayFromCenter: CGFloat = 25
static let hotspotSize: CGFloat = 30
static let detailHeight: CGFloat = 214
static let detailWidth: CGFloat = 500
static let arrowMargin: CGFloat = 9
static let arrowSize: CGFloat = 56
static let zoomRect: CGFloat = 200
static let overlayHeight: CGFloat = 267
}
enum playState {
case play
case shuffle
case favorites
}
protocol PlaySec: class {
}
class PlaySecVC: UIViewController, PlaySec {
// MARK: UIComponents
lazy var scrollView: UIScrollView = {
let _scrollView = UIScrollView(frame: .zero)
_scrollView.translatesAutoresizingMaskIntoConstraints = false
_scrollView.clipsToBounds = false
//_scrollView.isUserInteractionEnabled = true
return _scrollView
}()
lazy var imageView: UIImageView = {
let _imageView = UIImageView(frame: .zero)
_imageView.translatesAutoresizingMaskIntoConstraints = false
_imageView.contentMode = .scaleAspectFit
//_imageView.isUserInteractionEnabled = true
return _imageView
}()
lazy var beginOverlay: BeginOverlay = {
let _beginOverlay = BeginOverlay(frame: .zero)
_beginOverlay.translatesAutoresizingMaskIntoConstraints = false
return _beginOverlay
}()
lazy var detailView: UIView = {
let _detailView = UIView(frame: .zero)
_detailView.translatesAutoresizingMaskIntoConstraints = false
_detailView.isHidden = true
//_detailView.isUserInteractionEnabled = false
return _detailView
}()
lazy var leftArrow: UIButton = {
let _leftArrow = UIButton(frame: .zero)
_leftArrow.translatesAutoresizingMaskIntoConstraints = false
_leftArrow.isHidden = false
_leftArrow.setImage(#imageLiteral(resourceName: "Left-Arrow-Outline"), for: .normal)
return _leftArrow
}()
lazy var rightArrow: UIButton = {
let _rightArrow = UIButton(frame: .zero)
_rightArrow.translatesAutoresizingMaskIntoConstraints = false
_rightArrow.isHidden = false
_rightArrow.setImage(#imageLiteral(resourceName: "Right-Arrow-Outline"), for: .normal)
return _rightArrow
}()
var state: playState = .play
// MARK: Setup
private func setup() {
let viewController = self
}
private func setupConstraints() {
view.addSubview(scrollView)
scrollView.addSubview(imageView)
view.addSubview(detailView)
view.addSubview(beginOverlay)
view.addSubview(leftArrow)
view.addSubview(rightArrow)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
beginOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor),
beginOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -25),
beginOverlay.widthAnchor.constraint(equalToConstant: scvc.overlayWidth),
beginOverlay.heightAnchor.constraint(equalToConstant: scvc.overlayHeight),
detailView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
detailView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
detailView.heightAnchor.constraint(equalToConstant: scvc.detailHeight),
detailView.widthAnchor.constraint(equalToConstant: scvc.detailWidth),
leftArrow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: scvc.arrowMargin),
leftArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
leftArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
leftArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -1 * scvc.arrowMargin),
rightArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
rightArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
])
}
func favorite() {
}
func play() {
state = .play
}
func favoritesPlay() {
play()
state = .favorites
}
func shufflePlay() {
play()
state = .shuffle
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
setupConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
/*var touch: UITouch? = touches.first
if (touch?.view != detailView && !detailView.isHidden) {
detailView.isHidden = true
}*/
super.touchesBegan(touches, with: event)
}
}
Overlay:
fileprivate struct sizeConstants {
static let pillHeight: CGFloat = 38
static let pillCornerRadius: CGFloat = sizeConstants.pillHeight / 2
static let titleFontSize: CGFloat = 13
static let detailFontSize: CGFloat = 10
static let imageCenterToLeading: CGFloat = 3
static let circleDiameter: CGFloat = 66
static let circleRadius: CGFloat = sizeConstants.circleDiameter / 2
static let buttonTextHPadding: CGFloat = 4
static let buttonTextVPadding: CGFloat = 2
static let badgeSpacing: CGFloat = 5.5
static let titleBadgeSpacing: CGFloat = 19
static let badgeImageSize: CGFloat = 32
static let badgeTextFromCenter: CGFloat = 0
static let badgeTextToImage: CGFloat = 8
static let buttonBackgroundToText: CGFloat = 6
static let circleButtonSize: CGFloat = 48
static let rectButtonWidth: CGFloat = 36
static let rectButtonHeight: CGFloat = 39
static let badgesToButtons: CGFloat = 21.5
}
class ButtonView: UIView {
lazy var buttonBackgroundView: UIView = {
let _buttonBackgroundView = UIView(frame: .zero)
_buttonBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_buttonBackgroundView.backgroundColor = .black
_buttonBackgroundView.layer.cornerRadius = sizeConstants.circleRadius
return _buttonBackgroundView
}()
lazy var textBackgroundView: UIView = {
let _textBackgroundView = UIView(frame: .zero)
_textBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_textBackgroundView.backgroundColor = .black
_textBackgroundView.layer.cornerRadius = _textBackgroundView.frame.height / 2
return _textBackgroundView
}()
lazy var button: UIButton = {
let _button = UIButton(frame: .zero)
_button.translatesAutoresizingMaskIntoConstraints = false
return _button
}()
lazy var label: UILabel = {
let _label = UILabel(frame: .zero)
_label.translatesAutoresizingMaskIntoConstraints = false
_label.font = .systemFont(ofSize: 15)
_label.textColor = .white
return _label
}()
var isRect: Bool = false
convenience init(rect: Bool) {
self.init(frame: .zero)
self.isRect = rect
setupViews()
}
override func updateConstraints() {
NSLayoutConstraint.activate([
buttonBackgroundView.topAnchor.constraint(equalTo: topAnchor),
buttonBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonBackgroundView.widthAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
buttonBackgroundView.heightAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
button.centerXAnchor.constraint(equalTo: buttonBackgroundView.centerXAnchor),
button.centerYAnchor.constraint(equalTo: buttonBackgroundView.centerYAnchor),
textBackgroundView.topAnchor.constraint(equalTo: buttonBackgroundView.bottomAnchor, constant: sizeConstants.buttonBackgroundToText),
textBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
textBackgroundView.heightAnchor.constraint(equalTo: label.heightAnchor, constant: sizeConstants.buttonTextVPadding),
textBackgroundView.widthAnchor.constraint(equalTo: label.widthAnchor, constant: sizeConstants.buttonTextHPadding),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: textBackgroundView.centerYAnchor),
])
if (isRect) {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.rectButtonWidth),
button.heightAnchor.constraint(equalToConstant: sizeConstants.rectButtonHeight),
])
} else {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
button.heightAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
])
}
super.updateConstraints()
}
private func setupViews() {
addSubview(buttonBackgroundView)
addSubview(textBackgroundView)
addSubview(label)
addSubview(button)
label.sizeToFit()
setNeedsUpdateConstraints()
}
func setButtonProps(image: UIImage, text: String, target: Any, selector: Selector) {
self.button.addTarget(target, action: selector, for: .touchUpInside)
self.button.setImage(image, for: .normal)
self.label.text = text
}
#objc private func tapped() {
print("tapped")
}
}
class PlayOverlay: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
}
lazy var badgeStackView: UIStackView = {
let _badgeStackView = UIStackView(frame: .zero)
_badgeStackView.translatesAutoresizingMaskIntoConstraints = false
_badgeStackView.axis = .vertical
_badgeStackView.spacing = sizeConstants.badgeSpacing
_badgeStackView.distribution = .equalSpacing
return _badgeStackView
}()
lazy var buttonStackView: UIStackView = {
let _buttonStackView = UIStackView(frame: .zero)
_buttonStackView.translatesAutoresizingMaskIntoConstraints = false
_buttonStackView.axis = .horizontal
_buttonStackView.distribution = .equalSpacing
return _buttonStackView
}()
var vc: PlaySecVC!
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
NSLayoutConstraint.activate([
badgeStackView.topAnchor.constraint(equalTo: topAnchor, constant: sizeConstants.titleBadgeSpacing),
badgeStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
badgeStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.topAnchor.constraint(equalTo: badgeStackView.bottomAnchor, constant: sizeConstants.badgesToButtons),
buttonStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
super.updateConstraints()
}
}
class BeginOverlay: PlayOverlay {
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(badgeStackView)
addSubview(buttonStackView)
let shuffleButton = ButtonView(rect: false)
shuffleButton.setButtonProps(image: UIImage()/* replaced with empty image for demo */, text: "SHUFFLE", target: self, selector: #selector(shuffle))
let favoritesButton = ButtonView(rect: false)
favoritesButton.setButtonProps(image: UIImage()/* replaced with empty image for demo */, text: "FAVORITES", target: self, selector: #selector(favorites))
let playButton = ButtonView(rect: false)
playButton.setButtonProps(image: UIImage()/* replaced with empty image for demo */, text: "PLAY", target: self, selector: #selector(play))
buttonStackView.addArrangedSubview(shuffleButton)
buttonStackView.addArrangedSubview(favoritesButton)
buttonStackView.addArrangedSubview(playButton)
}
#objc private func shuffle() {
vc.shufflePlay()
}
#objc private func favorites() {
vc.favoritesPlay()
}
#objc private func play() {
vc.play()
}
}
I did some research and I figured out that since there are 2 UIStackView inside BeginOverlay, there is position ambiguity for the second one that contains the 3 UIButton. The image below may help.
Here is a place of fix. Tested with Xcode 11.4 / iOS 13.4
lazy var buttonStackView: UIStackView = {
let _buttonStackView = UIStackView(frame: .zero)
_buttonStackView.translatesAutoresizingMaskIntoConstraints = false
_buttonStackView.axis = .horizontal
_buttonStackView.distribution = .fillEqually // << here !!!
return _buttonStackView
}()
Here is complete tested module (for comparison, just in case I changed anything else). Just created single view iOS project from template and assign controller class in Storyboard.
fileprivate struct scvc {
static let overlayWidth: CGFloat = 330
static let overlayFromCenter: CGFloat = 25
static let hotspotSize: CGFloat = 30
static let detailHeight: CGFloat = 214
static let detailWidth: CGFloat = 500
static let arrowMargin: CGFloat = 9
static let arrowSize: CGFloat = 56
static let zoomRect: CGFloat = 200
static let overlayHeight: CGFloat = 267
}
enum playState {
case play
case shuffle
case favorites
}
protocol PlaySec: class {
}
class PlaySecVC: UIViewController, PlaySec {
// MARK: UIComponents
lazy var scrollView: UIScrollView = {
let _scrollView = UIScrollView(frame: .zero)
_scrollView.translatesAutoresizingMaskIntoConstraints = false
_scrollView.clipsToBounds = false
//_scrollView.isUserInteractionEnabled = true
return _scrollView
}()
lazy var imageView: UIImageView = {
let _imageView = UIImageView(frame: .zero)
_imageView.translatesAutoresizingMaskIntoConstraints = false
_imageView.contentMode = .scaleAspectFit
//_imageView.isUserInteractionEnabled = true
return _imageView
}()
lazy var beginOverlay: BeginOverlay = {
let _beginOverlay = BeginOverlay(frame: .zero)
_beginOverlay.translatesAutoresizingMaskIntoConstraints = false
return _beginOverlay
}()
lazy var detailView: UIView = {
let _detailView = UIView(frame: .zero)
_detailView.translatesAutoresizingMaskIntoConstraints = false
_detailView.isHidden = true
//_detailView.isUserInteractionEnabled = false
return _detailView
}()
lazy var leftArrow: UIButton = {
let _leftArrow = UIButton(frame: .zero)
_leftArrow.translatesAutoresizingMaskIntoConstraints = false
_leftArrow.isHidden = false
_leftArrow.setImage(UIImage(systemName: "arrow.left")!, for: .normal)
return _leftArrow
}()
lazy var rightArrow: UIButton = {
let _rightArrow = UIButton(frame: .zero)
_rightArrow.translatesAutoresizingMaskIntoConstraints = false
_rightArrow.isHidden = false
_rightArrow.setImage(UIImage(systemName: "arrow.right")!, for: .normal)
return _rightArrow
}()
var state: playState = .play
// MARK: Setup
private func setup() {
// let viewController = self
self.beginOverlay.vc = self
}
private func setupConstraints() {
view.addSubview(scrollView)
scrollView.addSubview(imageView)
view.addSubview(detailView)
view.addSubview(beginOverlay)
view.addSubview(leftArrow)
view.addSubview(rightArrow)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
beginOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor),
beginOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -25),
beginOverlay.widthAnchor.constraint(equalToConstant: scvc.overlayWidth),
beginOverlay.heightAnchor.constraint(equalToConstant: scvc.overlayHeight),
detailView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
detailView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
detailView.heightAnchor.constraint(equalToConstant: scvc.detailHeight),
detailView.widthAnchor.constraint(equalToConstant: scvc.detailWidth),
leftArrow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: scvc.arrowMargin),
leftArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
leftArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
leftArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -1 * scvc.arrowMargin),
rightArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
rightArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
])
}
func favorite() {
}
func play() {
state = .play
}
func favoritesPlay() {
play()
state = .favorites
}
func shufflePlay() {
play()
state = .shuffle
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
setupConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
/*var touch: UITouch? = touches.first
if (touch?.view != detailView && !detailView.isHidden) {
detailView.isHidden = true
}*/
super.touchesBegan(touches, with: event)
}
}
fileprivate struct sizeConstants {
static let pillHeight: CGFloat = 38
static let pillCornerRadius: CGFloat = sizeConstants.pillHeight / 2
static let titleFontSize: CGFloat = 13
static let detailFontSize: CGFloat = 10
static let imageCenterToLeading: CGFloat = 3
static let circleDiameter: CGFloat = 66
static let circleRadius: CGFloat = sizeConstants.circleDiameter / 2
static let buttonTextHPadding: CGFloat = 4
static let buttonTextVPadding: CGFloat = 2
static let badgeSpacing: CGFloat = 5.5
static let titleBadgeSpacing: CGFloat = 19
static let badgeImageSize: CGFloat = 32
static let badgeTextFromCenter: CGFloat = 0
static let badgeTextToImage: CGFloat = 8
static let buttonBackgroundToText: CGFloat = 6
static let circleButtonSize: CGFloat = 48
static let rectButtonWidth: CGFloat = 36
static let rectButtonHeight: CGFloat = 39
static let badgesToButtons: CGFloat = 21.5
}
class ButtonView: UIView {
lazy var buttonBackgroundView: UIView = {
let _buttonBackgroundView = UIView(frame: .zero)
_buttonBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_buttonBackgroundView.backgroundColor = .black
_buttonBackgroundView.layer.cornerRadius = sizeConstants.circleRadius
return _buttonBackgroundView
}()
lazy var textBackgroundView: UIView = {
let _textBackgroundView = UIView(frame: .zero)
_textBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_textBackgroundView.backgroundColor = .black
_textBackgroundView.layer.cornerRadius = _textBackgroundView.frame.height / 2
return _textBackgroundView
}()
lazy var button: UIButton = {
let _button = UIButton(frame: .zero)
_button.translatesAutoresizingMaskIntoConstraints = false
return _button
}()
lazy var label: UILabel = {
let _label = UILabel(frame: .zero)
_label.translatesAutoresizingMaskIntoConstraints = false
_label.font = .systemFont(ofSize: 15)
_label.textColor = .white
return _label
}()
var isRect: Bool = false
convenience init(rect: Bool) {
self.init(frame: .zero)
self.isRect = rect
setupViews()
}
override func updateConstraints() {
NSLayoutConstraint.activate([
buttonBackgroundView.topAnchor.constraint(equalTo: topAnchor),
buttonBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonBackgroundView.widthAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
buttonBackgroundView.heightAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
button.centerXAnchor.constraint(equalTo: buttonBackgroundView.centerXAnchor),
button.centerYAnchor.constraint(equalTo: buttonBackgroundView.centerYAnchor),
textBackgroundView.topAnchor.constraint(equalTo: buttonBackgroundView.bottomAnchor, constant: sizeConstants.buttonBackgroundToText),
textBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
textBackgroundView.heightAnchor.constraint(equalTo: label.heightAnchor, constant: sizeConstants.buttonTextVPadding),
textBackgroundView.widthAnchor.constraint(equalTo: label.widthAnchor, constant: sizeConstants.buttonTextHPadding),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: textBackgroundView.centerYAnchor),
])
if (isRect) {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.rectButtonWidth),
button.heightAnchor.constraint(equalToConstant: sizeConstants.rectButtonHeight),
])
} else {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
button.heightAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
])
}
super.updateConstraints()
}
private func setupViews() {
addSubview(buttonBackgroundView)
addSubview(textBackgroundView)
addSubview(label)
addSubview(button)
label.sizeToFit()
setNeedsUpdateConstraints()
}
func setButtonProps(image: UIImage, text: String, target: Any, selector: Selector) {
self.button.addTarget(target, action: selector, for: .touchUpInside)
self.button.setImage(image, for: .normal)
self.label.text = text
}
#objc private func tapped() {
print("tapped")
}
}
class PlayOverlay: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
}
lazy var badgeStackView: UIStackView = {
let _badgeStackView = UIStackView(frame: .zero)
_badgeStackView.translatesAutoresizingMaskIntoConstraints = false
_badgeStackView.axis = .vertical
_badgeStackView.spacing = sizeConstants.badgeSpacing
_badgeStackView.distribution = .equalSpacing
return _badgeStackView
}()
lazy var buttonStackView: UIStackView = {
let _buttonStackView = UIStackView(frame: .zero)
_buttonStackView.translatesAutoresizingMaskIntoConstraints = false
_buttonStackView.axis = .horizontal
_buttonStackView.distribution = .fillEqually
return _buttonStackView
}()
var vc: PlaySecVC!
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
NSLayoutConstraint.activate([
badgeStackView.topAnchor.constraint(equalTo: topAnchor, constant: sizeConstants.titleBadgeSpacing),
badgeStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
badgeStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.topAnchor.constraint(equalTo: badgeStackView.bottomAnchor, constant: sizeConstants.badgesToButtons),
buttonStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
super.updateConstraints()
}
}
class BeginOverlay: PlayOverlay {
override init(frame: CGRect) {
super.init(frame: .zero)
self.setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(badgeStackView)
addSubview(buttonStackView)
let shuffleButton = ButtonView(rect: false)
shuffleButton.setButtonProps(image: UIImage(systemName: "shuffle")!/* replaced with empty image for demo */, text: "SHUFFLE", target: self, selector: #selector(shuffle))
let favoritesButton = ButtonView(rect: false)
favoritesButton.setButtonProps(image: UIImage(systemName: "bookmark")!/* replaced with empty image for demo */, text: "FAVORITES", target: self, selector: #selector(favorites))
let playButton = ButtonView(rect: false)
playButton.setButtonProps(image: UIImage(systemName: "play")!/* replaced with empty image for demo */, text: "PLAY", target: self, selector: #selector(play))
buttonStackView.addArrangedSubview(shuffleButton)
buttonStackView.addArrangedSubview(favoritesButton)
buttonStackView.addArrangedSubview(playButton)
}
#objc private func shuffle() {
vc.shufflePlay()
}
#objc private func favorites() {
vc.favoritesPlay()
}
#objc private func play() {
vc.play()
}
}
Note: as mentioned it is better to review all constrains and fix run-time ambiguities.

Resources