4

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!

Bobbyphtr
  • 115
  • 6
  • UIStackView doesn't implement intrinsicContentSize, which probably is the source of your issues. If you add the textfield and error label to your view directly (and update the layout constraints accordingly) your approach should work. – Jack Goossen Mar 20 '23 at 15:50

1 Answers1

0

I prefer to use a UIViewControllerRepresentable instead of UIViewRepresentable

Because you can use constraints to "pin" a UIView to the edges. This allows SwiftUI to control the size.

struct Wrapper_UI: UIViewControllerRepresentable{
    typealias UIViewControllerType = WrapperVC
    let uiView: () -> UIView
    func makeUIViewController(context: Context) -> WrapperVC {
        let vc = WrapperVC(uiView: uiView())
        return vc
    }
    func updateUIViewController(_ uiViewController: WrapperVC, context: Context) {
        
    }
}
class WrapperVC: UIViewController{
    let uiView: UIView
    
    init(uiView: UIView) {
        self.uiView = uiView
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(uiView)
        uiView.pinToSuperview(self.view)
    }
}

extension UIView{
    func pinToSuperview(_ superView: UIView){
        self.translatesAutoresizingMaskIntoConstraints = false
        self.topAnchor.constraint(equalTo: superView.topAnchor).isActive = true
        self.bottomAnchor.constraint(equalTo: superView.bottomAnchor).isActive = true
        self.leadingAnchor.constraint(equalTo: superView.leadingAnchor).isActive = true
        self.trailingAnchor.constraint(equalTo: superView.trailingAnchor).isActive = true
    }
}

Then you can use most of your code

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) { //Isn't being used here
//        super.init(frame: frame)
//        configureView()
//    }
    
    //Initilizer in SwiftUI is using this
    init(){
        super.init(frame: .init(origin: .zero, size: .init(width: 100, height: 100)))

        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()
        }
    }
    
    deinit {
        print("TextField deinit")
    }
    
}

And in SwiftUI you can use frame to control the size.

struct WrapperView: View {
    var body: some View {
        VStack(spacing: 16.0) {
            
            Text("Register")
                .font(.title)
            
            Wrapper_UI {
                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"
                return customTf
            }
            //Use frame to define size
            .frame(maxWidth: .infinity, maxHeight: 100)
            .background(Color.green)
            .border(Color.red)

            Wrapper_UI {
                let btn = UIButton(type: .system)
                btn.setTitle("Regicster", for: .normal)
                return btn
            }
            //Use frame to define size
            .frame(maxWidth: .infinity, maxHeight: 100)

        }
        .padding(.horizontal, 16.0)
 
    
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48