0

I want to create a custom textfield for SwiftUI by using UIViewRepresentable. To handle the didBeginEditing and didFinishEditing actions I subscribed to the publishers that I previously created as an extension to the UITextField class. So I put the cancellables variable as an inout argument but get the error "Cannot use mutating member on immutable value: 'self' is immutable".

Is there any other way to do this?

My custom TextField

struct TextFieldView: UIViewRepresentable {
    private var cancellables = Set<AnyCancellable>()
    private let label: String
    private let activeIcon: String
    private let inactiveIcon: String
    private let helperText: String?

    init(_ label: String, activeIcon: String, inactiveIcon: String, helperText: String?) {
        self.label = label
        self.activeIcon = activeIcon
        self.inactiveIcon = inactiveIcon
        self.helperText = helperText
    }

    func makeUIView(context: Context) -> MDCOutlinedTextField {
        return MDCOutlinedTextField()
    }

    func updateUIView(_ uiView: MDCOutlinedTextField, context: Context) {
        uiView.tintColor = .black

        // ...

        uiView.didBeginEditingPublisher.sink { _ in
            uiView.leadingView = UIImageView(image: UIImage(named: activeIcon))
        }.store(in: &cancellables) // ERROR: Cannot use mutating member on immutable value: 'self' is immutable

        uiView.didFinishEditingPublisher.sink { _ in
            uiView.leadingView = UIImageView(image: UIImage(named: inactiveIcon))
        }.store(in: &cancellables) // ERROR: Cannot use mutating member on immutable value: 'self' is immutable
    }
}

UITextField extension

public extension UITextField {
    var didBeginEditingPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextField.textDidBeginEditingNotification, object: self)
            .map { ($0.object as? UITextField)?.text  ?? "" }
            .eraseToAnyPublisher()
    }

    var didFinishEditingPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextField.textDidEndEditingNotification, object: self)
            .map { ($0.object as? UITextField)?.text  ?? "" }
            .eraseToAnyPublisher()
    }
}
Heimdallr
  • 189
  • 1
  • 14
  • https://stackoverflow.com/search?q=%5Bswiftui%5D+Cannot+use+mutating+member+on+immutable+value%3A+%27self%27+is+immutable – matt Apr 20 '23 at 17:25
  • 1
    Creating Combine chains like this doesn't make sense inside `updateUIView`, which may get called *many* times. This is likely a better fit for a `Coordinator` (or a different architecture altogether) – jnpdx Apr 20 '23 at 17:30

1 Answers1

0

I solved the problem in the following way: Used the Coordinator in which I implemented the UITextFieldDelegate methods to catch the moment when user begins and finish editing

Thanks jnpdx for the tip

struct TextFieldView: UIViewRepresentable {
    private var cancellables = Set<AnyCancellable>()
    private let label: String
    private let activeIcon: String
    private let inactiveIcon: String
    private let helperText: String?

    init(_ label: String, activeIcon: String, inactiveIcon: String, helperText: String?) {
        self.label = label
        self.activeIcon = activeIcon
        self.inactiveIcon = inactiveIcon
        self.helperText = helperText
    }

    func makeCoordinator() -> Coordinator {
            Coordinator(activeIcon: activeIcon, inactiveIcon: inactiveIcon)
        }

    func makeUIView(context: Context) -> MDCOutlinedTextField {
        let view = MDCOutlinedTextField()
        view.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: MDCOutlinedTextField, context: Context) {
        uiView.tintColor = .black

        // ...
    }
}

extension TextFieldView {
    class Coordinator: NSObject, UITextFieldDelegate {
        private var activeIcon: String
        private var inactiveIcon: String

        init(activeIcon: String, inactiveIcon: String) {
            self.activeIcon = activeIcon
            self.inactiveIcon = inactiveIcon
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            let textField = textField as? MDCOutlinedTextField
            textField?.leadingView = UIImageView(image: UIImage(named: activeIcon))
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            let textField = textField as? MDCOutlinedTextField
            textField?.leadingView = UIImageView(image: UIImage(named: inactiveIcon))
        }
    }
}
Heimdallr
  • 189
  • 1
  • 14