iOS Custom control with dynamic elements - ios

I've started to build my first open source project I would like to share to a world. It's a simple Audio Player view which will work with AudioProvidable protocol and then will play an audio data after it's received.
For now I have an IBDesignable class inherits from UIView which builds itself by defining constraints for its subviews like this:
override init(frame: CGRect) {
super.init(frame: frame)
#if !TARGET_INTERFACE
translatesAutoresizingMaskIntoConstraints = false
#endif
prepareView()
updateUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepareView()
updateUI()
}
func prepareView() {
//Necessary in order to set our own constraints
//btnPlay is already doing the same inside buttonWithImage(_:)
spinner.translatesAutoresizingMaskIntoConstraints = false
sliderAudioProgress.translatesAutoresizingMaskIntoConstraints = false
//Add necessary subviews to start setting up layout
addSubview(sliderAudioProgress)
addSubview(lblAudioDuration)
addSubview(lblAudioProgress)
addSubview(spinner)
addSubview(btnPlay)
addSubview(lblTrackNumber)
addSubview(btnNextTrack)
addSubview(btnPreviousTrack)
//Height of play & next/previous track buttons defines AudioPlayerView height
let viewHeight:CGFloat = 48 * 2
//Setup UI layout using constraints
NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: viewHeight),
//Play button constraints
btnPlay.topAnchor.constraint(equalTo: topAnchor),
btnPlay.leadingAnchor.constraint(equalTo: leadingAnchor),
//Spinner constraints
spinner.centerYAnchor.constraint(equalTo: btnPlay.centerYAnchor),
spinner.centerXAnchor.constraint(equalTo: btnPlay.centerXAnchor),
//Progress label constraints
lblAudioProgress.leadingAnchor.constraint(equalTo: btnPlay.trailingAnchor),
lblAudioProgress.centerYAnchor.constraint(equalTo: btnPlay.centerYAnchor),
//Duration label constraints
lblAudioDuration.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -spacing),
lblAudioDuration.centerYAnchor.constraint(equalTo: btnPlay.centerYAnchor),
//Audio Progress Slider constraints
sliderAudioProgress.leadingAnchor.constraint(equalTo: lblAudioProgress.trailingAnchor, constant: spacing),
sliderAudioProgress.trailingAnchor.constraint(equalTo: lblAudioDuration.leadingAnchor, constant: -spacing),
sliderAudioProgress.centerYAnchor.constraint(equalTo: btnPlay.centerYAnchor),
lblTrackNumber.centerXAnchor.constraint(equalTo: centerXAnchor),
btnPreviousTrack.topAnchor.constraint(equalTo: btnPlay.bottomAnchor),
btnPreviousTrack.trailingAnchor.constraint(equalTo: lblTrackNumber.leadingAnchor, constant: -spacing),
btnNextTrack.topAnchor.constraint(equalTo: btnPlay.bottomAnchor),
btnNextTrack.leadingAnchor.constraint(equalTo: lblTrackNumber.trailingAnchor, constant: spacing),
lblTrackNumber.centerYAnchor.constraint(equalTo: btnNextTrack.centerYAnchor)
])
//Spinner setup:
spinner.hidesWhenStopped = true
//btnPlay setup:
btnPlay.addTarget(self, action: #selector(AudioPlayerView.playButtonPressed), for: .touchUpInside)
//Slider setup
sliderAudioProgress.thumbTintColor = UIColor(keyFromAppColorPalette: "secondary_color")
sliderAudioProgress.addTarget(self, action: #selector(AudioPlayerView.durationSliderValueChanged), for: .valueChanged)
sliderAudioProgress.addTarget(self, action: #selector(AudioPlayerView.durationSliderReleased), for: .touchUpInside)
sliderAudioProgress.isEnabled = false
sliderAudioProgress.isContinuous = true
sliderAudioProgress.semanticContentAttribute = .playback
//View's UI effects to make it look better
layer.cornerRadius = 6
layer.masksToBounds = true
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 2
semanticContentAttribute = .playback
spinner.startAnimating()
btnPlay.isHidden = true
}
As a result that is what we have when audio is receiving:
After the audio received:
In case when there's only one audio to play the bottom part, switches between tracks is redundant. Where do I have to put a checking code for audio tracks count to redesign my UI? I don't want to call prepareView() in didSet {} of audio source array, because I don't want existing elements be added twice.
Regards.

You could add a bool flag to see if the code is running for the first time, or, what I find easier and maybe a little more logical...
func prepareView() {
//Necessary in order to set our own constraints
//btnPlay is already doing the same inside buttonWithImage(_:)
spinner.translatesAutoresizingMaskIntoConstraints = false
sliderAudioProgress.translatesAutoresizingMaskIntoConstraints = false
// if our subviews have not yet been added
if sliderAudioProgress.superview == nil {
//Add necessary subviews to start setting up layout
addSubview(sliderAudioProgress)
addSubview(lblAudioDuration)
... etc
}
// continue here with any "every time" setup tasks
}

Related

Swift - pick image and label for UICollectionViewCell

right now I have a collectionView in which I can add cells by tapping the "addCell".
My goal is that if the user taps the "addCell" a view should appear where the user can type in a title for the cell and select an image like so:
Any idea on how I could realize that?
class ContentCell: UICollectionViewCell {
let testImage: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
let testLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "Test Label"
v.font = UIFont(name: "Avenir Next-Bold", size: 18)
v.textColor = .darkGray
v.textAlignment = .center
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(testLabel)
contentView.addSubview(testImage)
NSLayoutConstraint.activate([
testImage.topAnchor.constraint(equalTo: contentView.topAnchor),
testImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
testImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
testImage.heightAnchor.constraint(equalToConstant:150),
testLabel.topAnchor.constraint(equalTo: testImage.bottomAnchor,constant: 1),
testLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
testLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
testLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}
so in didSelectItemAtIndexPath you present the view controller to ask for the image image and title, and then in that view controller you declare a delegate or completion handler that when the user presses "done" or "save" in the view controller where they selected the image and title, the delegate or completion handler is then called and then tells the original view controller with the cells to add the item you just create and reload the collectionView. this is pretty standard stuff and this same pattern you'll need to learn how to use since most apps use this this same pattern 100s of times per app. i can show you from Objective-C but won't waste my time showing this is Swift. lmk if you need more help. good luck
oh yes, and for future reference, it's best to stick with delegation since using completion handlers gets very tricky when you encounter cases with nested completions and still need to maintain a weakified then strongified reference to the controller. after two degress of weakifying and strogifying "self" the system will start to randomly deallocate your controllers. Stick with delegation.

Cant get UISwitch constraints to work - swift 4

I'm trying to create a UISwitch programmatically but for some reason I can't get any constraints to work. If I don't use any constraints the UISwitch is created but in the top left corner.
I tried creating the UISwitch by adding in the constraints when declaring my switch and this does move the switch to a different place. However this is a problem as I want to it to be constrained against other items in my view.
class cameraController: UIViewController {
var switchOnOff: UISwitch {
let switchOnOff = UISwitch()
switchOnOff.isOn = false
switchOnOff.translatesAutoresizingMaskIntoConstraints = false
switchOnOff.addTarget(self, action: #selector(switchButton(_:)), for: .valueChanged)
return switchOnOff
}
}
extension cameraController{
#objc func switchButton(_ sender:UISwitch){
if (sender.isOn == true){
print("UISwitch state is now ON")
}
else{
print("UISwitch state is now Off")
}
}
}
extension cameraController {
private func setupLayouts() {
view.addSubview(switchOnOff)
switchOnOff.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
switchOnOff.topAnchor.constraint(equalTo: photosButtonIcon.bottomAnchor, constant: 20).isActive = true
}
}
I want to be able to add my constraints for the switch into my setupLayouts extension and not when the switch is being declared.
The error I am getting when running the above code is:
Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
I am getting this error on the first line of the constraints in my setupLayouts extension.
Any help would be great.
You need a lazy var to have self inside this block which is a closure , your current code is a computed property which returns a switch for every reference and that causes a crash in constraints as the switch added to view is not the same object as the one you set constraints for
lazy var switchOnOff: UISwitch = {
let switchOnOff = UISwitch()
switchOnOff.isOn = false
switchOnOff.translatesAutoresizingMaskIntoConstraints = false
switchOnOff.addTarget(self, action: #selector(switchButton(_:)), for: .valueChanged)
return switchOnOff
}()
Then call setupLayouts inside viewDidLoad , btw extension is totally irrelevant to this problem

Swift UILabel Subview not updating

I have a header of a UICollectionView. This header contains a UILabel. I'm updating the text value of the label by calling a function updateClue through a delegate.
class LetterTileHeader: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var clueLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16)
label.text = "HELLO"
label.textColor = UIColor.white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
func setupViews() {
backgroundColor = GlobalConstants.colorDarkBlue
addSubview(clueLabel)
clueLabel.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
clueLabel.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
clueLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
clueLabel.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
}
func updateClue(clue: String) {
clueLabel.text = clue
debugLabel()
}
func debugLabel() {
print(clueLabel.text)
}
The function debugLabel is printing with the changed text coming from updateClue. But the label on the screen does not update. What am I missing? I've tried using a DispatchQueue.main.async block with changing the label text within that but no still no change.
UpdateClue function is triggered from another class via a Protocol Delegation.
extension PuzzleViewController: WordTileDelegate {
func relaySelectedWordId(wordId: String!) {
selectableTiles.updateTileHeaderClue(wordId: wordId)
}
}
Thanks.
UPDATE:
I've figured out what's going on. This subview is created each time I select a new cell. Looking into why that's happening. Will update soon.
The issue I'm having is due to this cell class being initialized multiple times. I believe the label I'm seeing is not the current one being updated, but the previous label that was instantiated. I have a new issue though which I may open a question on if I can't figure it out.
You have two UILabels and/or two LetterTileHeaders. The one you are updating is not the one that is being displayed on the screen.
One way to prove this is to put a breakpoint on the line after let label = UILabel(). It is very likely that the breakpoint will be hit more than once.
Another way would be to look at the screen layout in the debugger and find the memory location of the UILabel that is getting displayed, then put a breakpoint in the updateClue method and compare that label's memory location to the one that is being displayed on the screen.
Follow code seems to be wrong, this mean each time when you use clueLble it will give you fresh UILable instance.
var clueLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16)
label.text = "HELLO"
label.textColor = UIColor.white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
Try doing this :
just replace above code with
var clueLabel = UILabel()
Add following func:
override func awakeFromNib() {
super.awakeFromNib()
label.font = UIFont.systemFont(ofSize: 16)
label.text = "HELLO"
label.textColor = UIColor.white
label.translatesAutoresizingMaskIntoConstraints = false
backgroundColor = GlobalConstants.colorDarkBlue
addSubview(clueLabel)
clueLabel.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
clueLabel.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
clueLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
clueLabel.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
}
remove setupViews()

UIStackView & Gestures

I'm trying to get/keep a handle on elements in my UIStackView after I have moved it with a pan gesture.
For example, the elements I am trying to grab a handle on are things like the button tag or the text label text.
The code explained…
I am creating a UIStackView, via the function func createButtonStack(label: String, btnTag: Int) -> UIStackView
It contains a button and a text label.
When the button stack is created, I attach a pan gesture to it so I can move the button around the screen. The following 3 points work.
I get the button stack created
I can press the button and call the fun to print my message.
I can move the button stack.
The issue I have is…
Once I move the button stack the first time and the if statement gesture.type == .ended line is triggered, I lose control of the button stack.
That is, the button presses no longer work nor can I move it around any longer.
Can anyone please help? Thanks
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
let ButtonStack = createButtonStack(label: “Button One”, btnTag: 1)
view.addSubview(ButtonStack)
ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
ButtonStack.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
let panGuesture = UIPanGestureRecognizer(target: self, action: #selector(pan(guesture:)))
ButtonStack.isUserInteractionEnabled = true
ButtonStack.addGestureRecognizer(panGuesture)
}
func createButtonStack(label: String, btnTag: Int) -> UIStackView {
let button = UIButton()
button.setImage( imageLiteral(resourceName: "star-in-circle"), for: .normal)
button.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
button.contentMode = .scaleAspectFit
button.tag = btnTag
switch btnTag {
case 1:
button.addTarget(self, action: #selector(printMessage), for: .touchUpInside)
case 2:
break
default:
break
}
//Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.green
textLabel.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
textLabel.font = textLabel.font.withSize(15)
textLabel.text = label
textLabel.textAlignment = .center
//Stack View
let buttonStack = UIStackView()
buttonStack.axis = UILayoutConstraintAxis.vertical
buttonStack.distribution = UIStackViewDistribution.equalSpacing
buttonStack.alignment = UIStackViewAlignment.center
buttonStack.spacing = 1.0
buttonStack.addArrangedSubview(button)
buttonStack.addArrangedSubview(textLabel)
buttonStack.translatesAutoresizingMaskIntoConstraints = false
return buttonStack
}
#objc func printMessage() {
print(“Button One was pressed”)
}
#objc func pan(guesture: UIPanGestureRecognizer) {
let translation = guesture.translation(in: self.view)
if let guestureView = guesture.view {
guestureView.center = CGPoint(x: guestureView.center.x + translation.x, y: guestureView.center.y + translation.y)
if guesture.state == .ended {
print("Guesture Center - Ended = \(guestureView.center)")
}
}
guesture.setTranslation(CGPoint.zero, in: self.view)
}
If you're using autolayout on the buttonStack you can't manipulate the guestureView.center centerX directly. You have to work with the constraints to achieve the drag effect.
So instead of ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true you should do something along the lines of:
let centerXConstraint = ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
centerXConstraint.isActive = true
ButtonStack.centerXConstraint = centerXConstraint
To do it like this you should declare a weak property of type NSLayoutConstraint on the ButtonStack class. You can do the same thing for the centerY constraint.
After that in the func pan(guesture: UIPanGestureRecognizer) method you can manipulate the centerXConstraint and centerYConstraint properties directly on the ButtonStack view.
Also, I see you are not setting the translatesAutoresizingMaskIntoConstraints property to false on the ButtonStack. You should do that whenever you are using autolayout programatically.
Thanks to PGDev and marosoaie for their input. Both provided insight for me to figure this one out.
My code worked with just the one button, but my project had three buttons inside a UIStackView.
Once I moved one button, it effectively broke the UIStackView and I lost control over the moved button.
The fix here was to take the three buttons out of the UIStackView and I can now move and control all three buttons without issues.
As for keeping a handle on the button / text field UIStackView, this was achieved by adding a .tag to the UIStackView.
Once I moved the element, the .ended action of the pan could access the .tag and therefore allow me to identify which button stack was moved.
Thanks again for all of the input.

Swift-UICollectionView in UIView Container

I have three sections, the header and a container where a tab bar can facilitate transition of pages all within that container. I want to build a UI like the figure below.
My approach is to create two container views one for the header and the other for tabbar and selected pages. The tabbar and selected pages are UICollectionViewCell. So I will be putting UICollectionViewCell in a UIView container.
in my BusinessHomeViewController.swift file
class BusinessHomeViewController: UIViewController {
let businessPagesContainer: BusinessPages = {
let bp = BusinessPages()
return bp
}()
func setupbusinessPagesContainerView(){
businessPagesContainer.heightAnchor.constraint(equalToConstant: 80).isActive = true
businessPagesContainer.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
businessPagesContainer.topAnchor.constraint(equalTo: businessDescriptionView.bottomAnchor, constant: 90).isActive = true
view.addSubview(menuBar)
menuBar.topAnchor.constraint(equalTo: pageToggleContainer.topAnchor, constant: 2).isActive = true
menuBar.centerXAnchor.constraint(equalTo: pageToggleContainer.centerXAnchor).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(businessPagesContainer)
setupbusinessPagesContainerView()
}
}
In my BusinessPages.swift file
import UIKit
class BusinessPages: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When I run the app, it crashes and gave this error "terminating with uncaught exception of type NSException". Is my approach wrong and how can I fix this?
If you're running into this problem make sure that you go to Main.storyboard, RIGHT click on the yellow box icon (view controller) at the top of the phone outline and DELETE the outlet(s) with yellow flags.

Resources