swiftswiftuicombineuiviewrepresentable

UIViewRepresentable: Cannot use mutating member on immutable value: 'self' is immutable


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()
    }
}

Solution

  • 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))
            }
        }
    }