Whenever I'm looking for content on how to build something, I often stumble upon videos and articles that stop at the classic "Hello World." Today, we’ll go a bit further.
As the title suggests, the goal is to add a bit more complexity when working with ViewCode.
In this article, I won’t cover the initial setup, as I’ve already discussed it in two previous articles: Getting Started with ViewCode in iOS: A Basic Guide and Setting Up ViewCode Projects for Versions Below iOS 13.
Let’s Get to Work
Creating the Main View
The idea is to replicate as much as possible from the native calculator app, especially regarding its functionalities.
To start, let's create a new file that will be responsible for managing the views of our application. Let’s name it CalculatorView.swift
.
import UIKit
class CalculatorView: UIView {}
Now, in our controller, we’ll set our newly created UIView as the main view of our UIViewController
.
private var calculatorView: CalculatorView? = nil
override func loadView() {
view = CalculatorView()
calculatorView = view as? CalculatorView
}
With this setup, we ensure that the views are handled within the view file, and the controller will only interact with them when necessary.
Creating the First Elements
Looking at the native calculator UI, we notice that the buttons are quite similar, differing only in some characteristics. Before thinking about how to customize them, let’s add the first button to the screen.
In our CalculatorView
, we’ll add the following elements:
private lazy var buttonContainer: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var button: UIButton = {
let button = UIButton()
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.font = UIFont.systemFont(ofSize: 40, weight: .medium)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
init() {
super.init(frame: .zero)
backgroundColor = .black
buttonContainer.backgroundColor = .darkGray
button.setTitle("1", for: .normal)
button.setTitleColor(.white, for: .normal)
addSubview(buttonContainer)
buttonContainer.addSubview(button)
NSLayoutConstraint.activate([
buttonContainer.centerYAnchor.constraint(equalTo: centerYAnchor),
buttonContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonContainer.heightAnchor.constraint(equalToConstant: 60),
buttonContainer.widthAnchor.constraint(equalToConstant: 60),
button.centerYAnchor.constraint(equalTo: buttonContainer.centerYAnchor),
button.centerXAnchor.constraint(equalTo: buttonContainer.centerXAnchor),
])
buttonContainer.layer.cornerRadius = 60 / 2
}
required init?(coder: NSCoder) {
fatalError()
}
Here, we’re doing the following:
- Creating a container for the button;
- Creating the button itself;
- In the view’s init, we apply the initial stylings, add the elements to the screen, and set up their constraints.
The result should look something like this:
Componentizing the Button
Now that we've taken the first step in building our components, let's create a separate button component, along with a small contract for our programmatic components.
The first step is creating a protocol to be followed by all our components. We can name it ViewCode.swift
.
protocol ViewCode {
func addSubviews()
func setupConstrainsts()
func setupStyles()
}
extension ViewCode {
func setup() {
addSubviews()
setupConstrainsts()
setupStyles()
}
}
Each component will implement three methods:
-
addSubviews
to add elements; -
setupConstraints
to configure the constraints; -
setupStyles
to style the elements.
With the setup
method, we create a unified way to call this set of methods.
Now, we can create the button component in CalculatorButtonView.swift
.
class CalculatorButtonView: UIView {
private lazy var button: UIButton = {
let button = UIButton()
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.font = UIFont.systemFont(ofSize: 40, weight: .medium)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
init() {
super.init(frame: .zero)
setup()
}
required init?(coder: NSCoder) {
fatalError()
}
func config(
text: String,
textColor: UIColor? = .white,
buttonColor: UIColor? = .darkGray
) {
backgroundColor = buttonColor
button.setTitle(text, for: .normal)
button.setTitleColor(textColor, for: .normal)
}
}
//MARK: - ViewCode
extension CalculatorButtonView: ViewCode {
func addSubviews() {
addSubview(button)
}
func setupConstrainsts() {
NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: 60),
widthAnchor.constraint(equalToConstant: 60),
button.centerYAnchor.constraint(equalTo: centerYAnchor),
button.widthAnchor.constraint(equalToConstant: 60),
button.heightAnchor.constraint(equalToConstant: 60),
])
}
func setupStyles() {
layer.cornerRadius = 60 / 2
translatesAutoresizingMaskIntoConstraints = false
}
}
The config
method will allow future customizations of the buttons as needed.
In CalculatorView
, we adjust the button positioning to ensure they are closer to their correct locations on the interface.
class CalculatorView: UIView {
private lazy var one: CalculatorButtonView = {
let one = CalculatorButtonView()
one.config(text: "1")
return one
}()
init() {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError()
}
private func oneConstraints() {
NSLayoutConstraint.activate([
one.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
one.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24),
])
}
}
extension CalculatorView: ViewCode {
func addSubviews() {
addSubview(one)
}
func setupConstrainsts() {
oneConstraints()
}
func setupStyles() {
backgroundColor = .black
}
}
In the controller, we call the setup method from CalculatorView
, as it now follows the established project pattern.
override func loadView() {
// ...
calculatorView?.setup()
}
With these changes we should have the following result.
Creating the Other Elements
Adding more simple elements to the screen, we might end up with something like this:
The button positioning followed this logic:
- Button "one" is anchored to the bottom of the screen and
16px
from theleadingAnchor
; - Button "two" is
16px
from thetrailingAnchor
of button "one" andcenterYAnchor
equal toone.centerYAnchor
; - Button "four" is
16px
from theleadingAnchor
and thebottomAnchor
16px
aboveone.topAnchor
.
I chose not to use UIStackView in this context.
You may have noticed that the buttons still don’t resemble the native app. That’s because the fixed 60px size makes them look rigid and disproportionate. To fix this, we need to calculate the button sizes based on the screen width.
The logic applied in CalculatorButtonView
is as follows: each element’s size will be the screen width minus the spacing, divided by 4. There are other approaches, but this will work for now.
static var elementWidth = (UIScreen.main.bounds.width - 5 * 16) / 4
I added this static
variable to make it easier to access this information even outside the context of the element.
And if we replace the 60px
we used in the file with CalculatorButtonView.elementWidth
. We will have the following result:
Much better, right?
Final Buttons and Result Label
To complete the UI, we need to add a few more buttons and the field where the calculation result will be displayed.
In the CalculatorView
file we create a new element:
private lazy var result: UILabel = {
let label = UILabel()
label.text = "0"
label.font = UIFont.systemFont(ofSize: 80)
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
return label
}()
// ...
private func resultConstraints() {
NSLayoutConstraint.activate([
result.bottomAnchor.constraint(equalTo: divide.topAnchor, constant: -24),
result.trailingAnchor.constraint(equalTo: divide.trailingAnchor, constant: -16),
result.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16)
])
}
// ...
func addSubviews() {
// ...
addSubview(result)
}
func setupConstrainsts() {
// ...
resultConstraints(result)
}
If we run the application again, we will have something like this:
Lastly, let's move on to creating the final buttons. To achieve this, we’ll need to make some adjustments to our CalculatorButtonView so that the elements don’t have fixed sizes.
We start with our config method, allowing us to change the element's alignment and size dynamically.
func config(
text: String,
textColor: UIColor? = .white,
buttonColor: UIColor? = .darkGray,
alignLeft: Bool = false,
width: CGFloat? = nil,
height: CGFloat? = nil
) {
backgroundColor = buttonColor
button.setTitle(text, for: .normal)
button.setTitleColor(textColor, for: .normal)
if (!alignLeft) {
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: centerXAnchor),
])
} else {
NSLayoutConstraint.activate([
button.titleLabel!.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 24),
])
}
NSLayoutConstraint.activate([
widthAnchor.constraint(equalToConstant: width ?? CalculatorButtonView.elementWidth),
button.widthAnchor.constraint(equalToConstant: width ?? CalculatorButtonView.elementWidth),
button.heightAnchor.constraint(equalToConstant: height ?? CalculatorButtonView.elementWidth),
])
}
With this modification we created the possibility of changing the element's alignment and size via the config method.
Important! We need to remove the constraints previously set in the setupConstraints method. If this isn't done, there will be conflicts due to multiple constraints being applied to the same element.
It will look like this:
func setupConstrainsts() {
NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: CalculatorButtonView.elementWidth),
button.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
Now, we can create our final elements in the CalculatorView
file.
private lazy var zero: CalculatorButtonView = {
let zero = CalculatorButtonView()
zero.config(text: "0", alignLeft: true, width: CalculatorButtonView.elementWidth * 2 + 16)
return zero
}()
private lazy var comma: CalculatorButtonView = {
let comma = CalculatorButtonView()
comma.config(text: ",")
return comma
}()
private lazy var equal: CalculatorButtonView = {
let equal = CalculatorButtonView()
equal.config(text: "=", buttonColor: .orange)
return equal
}()
As you can see, the "zero" button will be the size of two buttons plus the 16px spacing between elements.
Next, we can configure the constraints for these elements.
private func zeroConstraints() {
NSLayoutConstraint.activate([
zero.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
zero.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24),
])
}
private func commaConstraints() {
NSLayoutConstraint.activate([
comma.leadingAnchor.constraint(equalTo: zero.trailingAnchor, constant: 16),
comma.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24),
])
}
private func equalConstraints() {
NSLayoutConstraint.activate([
equal.leadingAnchor.constraint(equalTo: comma.trailingAnchor, constant: 16),
equal.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24),
])
}
Additionally, the constraint for the "one" button will need to change since it should be linked to the "zero" button rather than the screen itself.
private func oneConstraints() {
NSLayoutConstraint.activate([
one.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
one.bottomAnchor.constraint(equalTo: zero.topAnchor, constant: -16),
])
}
Por ultimo colocamos os elementos na tela e disparamos as configs que fizemos:
addSubview(zero)
addSubview(comma)
addSubview(equal)
//...
zeroConstraints()
commaConstraints()
equalConstraints()
Finally, we place the elements on the screen and apply the configurations we made.
With this last modification, when we run the app, the result should look something like this:
Interactivity
Now that our UI is ready, we need to add some interaction between the buttons and the result field at the top. We can follow these steps:
In the CalculatorButtonView file, we create a delegate to capture which button was pressed. We also configure the UIButton to trigger this callback.
protocol CalculatorButtonViewDelegate: AnyObject {
func didTapButton(_ sender: UIButton)
}
This allows us to capture which button was pressed. Still within this file we need to configure our UIButton with this callback:
private lazy var button: UIButton = {
// ...
button.addTarget(self, action: #selector(onTap), for: .touchUpInside)
return button
}()
weak var delegate: CalculatorButtonViewDelegate?
@objc
private func onTap(sender: UIButton) {
delegate?.didTapButton(sender)
}
These small changes make the component "clickable."
Next, in CalculatorView
, we set up the delegate for the buttons. Here, I opted for the willSet approach to configure all buttons at once.
weak var buttonsDelegate: CalculatorButtonViewDelegate? {
willSet {
one.delegate = newValue
two.delegate = newValue
three.delegate = newValue
plus.delegate = newValue
four.delegate = newValue
five.delegate = newValue
six.delegate = newValue
minus.delegate = newValue
seven.delegate = newValue
eight.delegate = newValue
nine.delegate = newValue
multiply.delegate = newValue
clear.delegate = newValue
positiveNegative.delegate = newValue
percent.delegate = newValue
divide.delegate = newValue
zero.delegate = newValue
comma.delegate = newValue
equal.delegate = newValue
}
}
Finally, in our controller, we implement the necessary logic to handle the interactions.
override func viewDidLoad() {
super.viewDidLoad()
// ...
calculatorView?.buttonsDelegate = self
}
extension ViewController: CalculatorButtonViewDelegate {
func didTapButton(_ sender: UIButton) {
if let buttonValue = sender.currentTitle {
print(buttonValue)
}
}
}
The result is:
Conclusion
With everything we've done so far, you now have the foundation to build the rest of the interactivity and logic for when each button is pressed. If you want to see the full project, check out the repository, where I’ve added some additional details not covered here, along with unit tests for the button logic.
Here’s how it turned out:
That’s it for today, folks. Thanks, and see you next time!
Let me know if you need any further adjustments!
Top comments (0)