In my iOS project, I’m working with a UIStackView that contains multiple subviews. I want to dynamically flip or move up and down specific views within the stack view based on a condition. Specifically,
I’m attempting to manipulate the positions of two text fields named FIRST and SECOND. However, I’m encountering an issue where the flipping only works correctly on every second tap or click.
Here is my code for the same
import UIKit
import Combine
import SnapKit
class ViewController: UIViewController {
private var toggleSubject = PassthroughSubject<Bool, Never>()
private var toggle = false
private var store = Set<AnyCancellable>()
private lazy var topField: AppTextField = {
let view = AppTextField(title: "First")
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var bottomField: AppTextField = {
let view = AppTextField(title: "Second")
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var lineView: UIImageView = {
let view = UIImageView(frame: .zero)
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var stackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [topField, lineView, bottomField, toggleButton])
view.axis = .vertical
view.spacing = 4.0
view.distribution = .fill
view.alignment = .fill
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var toggleButton: UIButton = {
let view = UIButton(frame: .zero)
view.setTitle("Toggle", for: .normal)
view.setTitleColor(.white, for: .normal)
view.setTitleColor(.systemYellow, for: .highlighted)
view.backgroundColor = .darkGray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var selectedTopFieldLabel: UILabel = {
let view = UILabel(frame: .zero)
view.textAlignment = .center
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(stackView)
self.view.addSubview(selectedTopFieldLabel)
toggleSubject.send(toggle)
stackView.snp.makeConstraints { make in
make.leading.trailing.centerY.equalToSuperview().inset(16)
}
lineView.snp.makeConstraints { make in
make.height.equalTo(0.5)
}
selectedTopFieldLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(200)
}
toggleButton.addTarget(self, action: #selector(toggleButtonAction), for: .touchUpInside)
toggleSubject.sink { toggle in
print("---> Changed Toggle",toggle)
self.updateUI(toggle: toggle)
UIView.animate(withDuration: 0.25) {
self.stackView.layoutIfNeeded()
}
}.store(in: &store)
self.updateUI(toggle: toggle)
}
@objc func toggleButtonAction() {
toggle.toggle()
toggleSubject.send(toggle)
}
private func updateUI(toggle: Bool) {
let arrangedSubviews = self.stackView.arrangedSubviews
guard var first = arrangedSubviews[0] as? AppTextField,
var second = arrangedSubviews[2] as? AppTextField else {return}
stackView.arrangedSubviews.forEach({$0.removeFromSuperview()})
let changedViews = toggle ? [first, lineView, second, toggleButton] : [second, lineView, first , toggleButton]
guard let firstTextField = changedViews.first as? AppTextField,
let firstfieldPlaceholder = firstTextField.placeholder else {return}
self.selectedTopFieldLabel.text = "Selected On Top " + " -> " + firstfieldPlaceholder
changedViews.forEach { view in
self.stackView.addArrangedSubview(view)
}
// Update constraints
UIView.animate(withDuration: 0.25) {
self.stackView.setNeedsLayout()
self.stackView.layoutIfNeeded()
}
}
}
class AppTextField: UIView {
private lazy var inputField: UITextField = {
let textField = UITextField(frame: .zero)
textField.backgroundColor = .lightGray.withAlphaComponent(0.15)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
var title: String? {
get { return inputField.placeholder }
set { inputField.placeholder = newValue }
}
var placeholder: String? {
get { return inputField.placeholder }
set { inputField.placeholder = newValue }
}
var text: String? {
get { return inputField.text }
set { inputField.text = newValue }
}
init(title: String) {
super.init(frame: .zero)
self.title = title
setupViews()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(inputField)
inputField.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(48)
}
}
}
Thanks in advance.
3
Answers
I could change the code above to
CurrentValueSubject<Bool, Never>(false)
instead to avoid extraBool
variableaddArrangedSubview
without removing sub views then re-adding againThe toggle action should be:
Then remove other stuff in
updateUI
:Output
Update: Since you wanted to keep your current code, I updated the answer to this approach below. The root problem is you remained arrangedSubviews at index 0 and 2 even when these AppTextField swapped each other, the indices need to swap too:
Your problem is in this line
It accesses the same item and put it again in same place because of
the ternary operator , you already swap them by accessing the indices so change it to
The problem is your logic…
When you tap the button, you toggle
toggle
between True and FalseThen you call
updateUI(toggle: toggle)
, at which point this code:Says:
toggle
isTrue
Which means your button taps are saying:
So, if your goal is to change the order on every button tap, you don’t need the
toggle
bool var at all. Change your UI update to this:and call it without the bool:
Or, if your goal is to set the order based on a Bool var, you might change your code to this:
and call it like this: