I'm giving up on finding sources on the internet because it is difficult to find my specific case. I'm also new to SwiftUI.
So I have a custom textfield wrapped within a custom UIView including its error text like this.
Basically it just an UITextField and UILabel wrapped inside an UIStackView and added into an UIView. The error label should follow to the next line when it reaches its max width.
I know it is much easier to recreate the whole thing on SwiftUI. But this is only a simplified example of the problem I'm facing.
CustomUITextField
class CustomUITextField: UIView {
// Public
var errorText: String? {
didSet {
isShowError(errorText != nil)
}
}
var placeholder: String? {
didSet {
textfield.placeholder = placeholder
}
}
lazy var textfield: UITextField = {
let textfield = UITextField()
textfield.translatesAutoresizingMaskIntoConstraints = false
return textfield
}()
private lazy var textfieldContainer: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.init(white: 0.95, alpha: 1.0)
view.layer.cornerRadius = 8.0
view.addSubview(textfield)
return view
}()
private lazy var errorTextLabel: UILabel = {
let label = UILabel()
label.textColor = .red
label.font = .systemFont(ofSize: 12.0)
label.isHidden = true
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
private lazy var mainStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [textfieldContainer])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fill
stackView.spacing = 4.0
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureView() {
self.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.topAnchor.constraint(equalTo: topAnchor),
mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
textfield.topAnchor.constraint(equalTo: textfieldContainer.topAnchor),
textfield.bottomAnchor.constraint(equalTo: textfieldContainer.bottomAnchor),
textfield.trailingAnchor.constraint(equalTo: textfieldContainer.trailingAnchor, constant: -12.0),
textfield.leadingAnchor.constraint(equalTo: textfieldContainer.leadingAnchor, constant: 12.0),
textfield.heightAnchor.constraint(equalToConstant: 55.0)
])
}
private func isShowError(_ isShow: Bool) {
errorTextLabel.text = errorText
errorTextLabel.isHidden = !isShow
mainStackView.addArrangedSubview(errorTextLabel)
DispatchQueue.main.async {
self.layoutIfNeeded()
}
}
// override func layoutSubviews() {
// super.layoutSubviews()
// invalidateIntrinsicContentSize()
// }
//
// var preferredIntrinsicContentWidth: CGFloat = .zero {
// didSet {
// invalidateIntrinsicContentSize()
// }
// }
// I refrain setting the value to a constant. I want the view self adjust to its container inside SwiftUI
// override var intrinsicContentSize: CGSize {
// return CGSize(width: UIScreen.main.bounds.width, height: 55.0)
// }
deinit {
print("TextField deinit")
}
}
But, now we want to adopt this into SwiftUI. I created a generic UIViewRepresentable wrapper for it.
Generic wrapper
protocol SwiftUICoordinator: AnyObject {}
struct WrapperViewSwiftUI<Wrapper : UIView>: UIViewRepresentable {
typealias SwiftUICoordinatorFactory = (() -> SwiftUICoordinator)
typealias ViewFactory = (_ context: Context) -> Wrapper
typealias Updater = ((Wrapper, Context) -> Void)
var makeView: ViewFactory
var coordinate: SwiftUICoordinatorFactory?
var update: Updater?
init(_ makeView: @escaping ViewFactory,
updater update: Updater? = nil,
coordinator coordinate: SwiftUICoordinatorFactory? = nil
) {
self.makeView = makeView
self.update = update
self.coordinate = coordinate
}
func makeUIView(context: Context) -> Wrapper {
makeView(context)
}
func updateUIView(_ view: Wrapper, context: Context) {
update?(view, context)
}
func makeCoordinator() -> SwiftUICoordinator? {
return coordinate?()
}
}
And using it like this.
Full implementation
struct RegisterView: View {
var body: some View {
VStack(spacing: 16.0) {
Text("Register")
.font(.title)
WrapperViewSwiftUI ({ _ in
let customTf = CustomUITextField()
customTf.placeholder = "Name"
customTf.errorText = "Username error text dkosdk sodk sokd osdk osdk sodk sokd sodk osdk oskdosdk sokdoskd osd kos kd"
customTf.translatesAutoresizingMaskIntoConstraints = false
return customTf
})
.fixedSize(horizontal: false, vertical: true)
.background(Color.green)
WrapperSwiftUI { _ in
let btn = CustomButton(style: .primary)
btn.setTitle("Register", for: .normal)
return btn
}
.fixedSize()
}
.padding(.horizontal, 16.0)
}
}
It was shown like this on simulator.
What I did
I know that using intrinsicContentSize
will help both preview and SwiftUI layouts its components. But I still don't understand on how to update these attributes to a correct size. It would be great if the SwiftUI follows the height of the CustomUITextField
.
If there is any wrong implementation somewhere please let me know! I'm open for every answer provided!