read

When I started out SwiftUI, I thought I will not need to go back to UIKit stuff. But I am wrong, for 2 reasons:

  1. SwiftUI is very incomplete
  2. SwiftUI is very buggy

SwiftUI is incomplete because it does not provide WebView, or MailComposeView, etc.

I can forgive for being incomplete since the framework is still in early days. But for being buggy, that didn’t speak well of Apple, again.

My last straw is with their TextField. It does not work well for Chinese input. You can never type more than a few characters before it becomes wonky.

I believe we gonna depend on UIKit’s components, for a long long time.

Wrapping UITextField

struct WrappedTextField: UIViewRepresentable {
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }

    class Coordinator: NSObject, UITextFieldDelegate {
    }
}

The code is made up of:

  1. 3 methods to implement SwiftUI UIViewRepresentable protocol
  2. A nested Coordinator for hooking up the delegate (optional)

That bare minimum code will solve the Chinese input bug, since it is the old and reliable UITextField.

To make it useful, the wrapper needs to add a Binding<String> for the SwiftUI world. The complete code to have a usable textfield is such:

struct WrappedTextField: UIViewRepresentable {
    @Binding var text: String // Declare a binding value

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

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text // 1. Read the binded
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        @Binding var text: String

        init(text: Binding<String>) {
            self._text = text
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            DispatchQueue.main.async {
                self.text = textField.text ?? "" // 2. Write to the binded
            }
        }
    }
}

The 2-way binding is provided by

  1. updateUIView to read the binded text
  2. In Coordinator, the delegate method textFieldDidChangeSelection will write to the binded text. Note that it is wrapped with a main queue dispatch because if not, there will be a warning:

    Modifying state during view update, this will cause undefined behavior

UPDATE: The text field is still cranky in when typing certain foreign languages. Another workaround is to use textFieldDidEndEditing, textFieldShouldClear and textFieldShouldReturn to update the binded text, with the tradeoff that it is not updated instantly. 🧙

An Xcode Template

Because much of the boilerplate code follows a strict pattern, I create a code snippet so that I can wrap UIView easily, and then focus my time on configuring the actual view.

struct <#WrappedUIView#>: UIViewRepresentable {
    @Binding var text: String

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

    func updateUIView(_ uiView: <#UIView#>, context: Context) {
        uiView.text = text
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }

    class Coordinator: NSObject, <#UIViewDelegate#> {
        @Binding var text: String

        init(text: Binding<String>) {
            self._text = text
        }

        func someDelegateMethod(_ uiView: UIView) {
            DispatchQueue.main.async {
                self.text = uiView.text ?? ""
            }
        }
    }
}

The code still has to be edited for the specific UIView purpose eg. instead of Binding<String> it could be other types, and the delegate methods.

Wrapping UIViewController

It is very similar for wrapping a UIViewController. In all the code that has a “-UIView”, replace it with “-UIViewController”. 🤓

Using Introspect

There is a very useful library that helps to access the underlying UIKit’s component. Introspect usage:

List {
    Text("We all know List is implemented using UITableView..")
}
.introspectTableView { tableView in
    // Do whatever you want with UITableView!
    tableView.separatorStyle = .none
}

Presenting SwiftUI view in UIViewController

let vc = UIHostingController(rootView: Text("Any SwiftUI View"))
present(vc, animated: true)

UIHostingController is part of SwiftUI framework, so import correctly.


Image

@samwize

¯\_(ツ)_/¯

Back to Home