Self-Resizing UIView inside SwiftUI

Viewed 40

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!

0 Answers
Related