I created a simple TextView like this:
import SwiftUI
struct TextView: UIViewRepresentable {
#Binding var inputText: String
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
return UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
// uiView.text = self.inputText
self.inputText = uiView.text
}
}
And the main view contains a variable with a #State wrapper.
import SwiftUI
struct Test: View {
#State private var inputText: String = ""
var body: some View {
VStack {
TextEditor(text: $inputText)
TextView(inputText: $inputText)
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
As I understand, the TextView and TextEditor are connected by this inputText, who has a #Binding wrapper in the definition of TextView. Thus if I type in one of them, another should show the same text. However, to my surprise, the text in these two are completely separated with each other.
If I change the self.inputText = uiView.text to uiView.text = self.inputText, then when I type in TextEditor, the TextView would show the same text, but if I type in TextView, then nothing happens in TextEditor.
Why does this happen?
Related
Overview: I'm using SwiftUI, but wanted to use UIKit-MapKit. I used UIViewRepresentable to be able to wrap the UIKit feature.
Problem: I'm learning about swiftui-uikit-interoperability and I'm getting stuck on being able to display multiple SwiftUI views.
Code Snippet:
ContentView
struct ContentView: View {
#ObservedObject var viewModel: MapView.PinViewModel
init() {
self.viewModel = MapView.PinViewModel()
}
var body: some View {
NavigationView {
MapView()
.sheet(isPresented: $viewModel.showPinForm) {
PinForm()
}
.navigationTitle("SwiftUI UIKit Interop").scaledToFill()
}
}
}
MapView
struct MapView: UIViewRepresentable {
class PinViewModel: ObservableObject {
#Published var showPinForm: Bool
init() {
self.showPinForm = false
}
func updateShowPinVar() {
self.showPinForm = true
}
}
func showPinForm() {
pinViewModel.updateShowPinVar()
}
func makeCoordinator() -> MapViewCoordinator {
let coordinator = MapViewCoordinator()
coordinator.delegate = self
return coordinator
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 40.7209, longitude: -74.0007)
let span = MKCoordinateSpan(latitudeDelta: 0.03, longitudeDelta: 0.03)
let mapRegion = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(mapRegion, animated: true)
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}
In this I have a #Published var showPinForm that gets toggled in MapView. ContentView is supposed to watch this variable and when it is true it will cause the sheet to pull up. However, I believe when I enter MapView() from ContentView() then I no longer recognize ContentView.
Using the UIViewRepresentable, what is the best way to display another swiftui view? Does not have to use .sheet (Although, it would be nice)
I have tried to simplify the code to show the main problem, so I left out a lot of additional info and took out basic patterns that I used (MVVM)
Please let me know if you need any clarifications
try to follow this pattern, you can toggle the flag both inside and outside your MapView
struct MapView: UIViewRepresentable {
#Binding var switcher: Bool // -> use binding
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct MainView: View {
#ObservedObject var viewModel = MainViewModel()
var body: some View {
MapView(switcher: $viewModel.flag)
.sheet(isPresented: $viewModel.flag) {
Text("Pin pin")
}
}
}
class MainViewModel: ObservableObject {
#Published var flag: Bool = false
}
I'm working with a wrapped UITextView via UIViewRepresentable. This textView is supposed to be as high es required, based on its content, i.e. the (attributed)string.
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isScrollEnabled = false
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
This works fine:
struct TextView_Previews: PreviewProvider {
static var previews: some View {
TextView(text: .constant("some multiline\na\nb\nc\nd\nlorem ipsum"))
}
}
Doesn't work
struct TextView_Previews: PreviewProvider {
static var previews: some View {
ScrollView { // <- when placed inside ScrollView the height is reduced to one line
TextView(text: .constant("some multiline\na\nb\nc\nd\nlorem ipsum"))
}
}
}
How can this be fixed?
struct TextView_Previews: PreviewProvider {
static var previews: some View {
ScrollView { // <- when placed inside ScrollView the height is reduced to one line
TextView(text: .constant("some multiline\na\nb\nc\nd\nlorem ipsum"))
.frame(minHeight: 100)
}
}
}
In my list, my UIViewRepresentable won't be updated if it is the only item in list. If I add e.g. a Text to it, it works. To see the effect, scroll down and up again.
What am i doing wrong?
Hers is my code:
struct Test : UIViewRepresentable {
var text : String
func makeUIView(context: UIViewRepresentableContext<Test>) -> UILabel {
UILabel()
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Test>) {
uiView.text = text
}
typealias UIViewType = UILabel
}
class Data : ObservableObject {
#Published var names = UIFont.familyNames
}
struct ContentView : View {
#EnvironmentObject var data : Data
var body: some View {
VStack {
List(data.names, id: \.self) { name in
Test(text: name)
// Text(name) // as soon as you comment this out, it works
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
For now I know variant/workaround that works (tested on all available Xcode 11.x)
var body: some View {
VStack {
List(data.names, id: \.self) { name in
Test(text: name).id(name) // << here !!
}
}
}
Note: it might be performance drop on very big lists, but in most usual cases it is not remarkable.
PS: By the way, about class Data - don't name your classes with same names as system one, there might be confuses and unexpectable issues.
I am using a SwiftUI TextField with a Binding String to change the user's input into a phone format. Upon typing, the formatting is happening, but the cursor isn't moved to the end of the textfield, it remains on the position it was when it was entered. For example, if I enter 1, the value of the texfield (after formatting) will be (1, but the cursor stays after the first character, instead of at the end of the line.
Is there a way to move the textfield's cursor to the end of the line?
Here is the sample code:
import SwiftUI
import AnyFormatKit
struct ContentView: View {
#State var phoneNumber = ""
let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
var body: some View {
let phoneNumberProxy = Binding<String>(
get: {
return (self.phoneFormatter.format(self.phoneNumber) ?? "")
},
set: {
self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
}
)
return TextField("Phone Number", text: phoneNumberProxy)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You might have to use UITextField instead of TextField. UITextField allows setting custom cursor position. To position the cursor at the end of the text you can use textField.endOfDocument to set UITextField.selectedTextRange when the text content is updated.
#objc func textFieldDidChange(_ textField: UITextField) {
let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
The following SwiftUI code snippet shows a sample implementation.
import SwiftUI
import UIKit
//import AnyFormatKit
struct ContentView: View {
#State var phoneNumber = ""
let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
var body: some View {
let phoneNumberProxy = Binding<String>(
get: {
return (self.phoneFormatter.format(self.phoneNumber) ?? "")
},
set: {
self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
}
)
return TextFieldContainer("Phone Number", text: phoneNumberProxy)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
/************************************************/
struct TextFieldContainer: UIViewRepresentable {
private var placeholder : String
private var text : Binding<String>
init(_ placeholder:String, text:Binding<String>) {
self.placeholder = placeholder
self.text = text
}
func makeCoordinator() -> TextFieldContainer.Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<TextFieldContainer>) -> UITextField {
let innertTextField = UITextField(frame: .zero)
innertTextField.placeholder = placeholder
innertTextField.text = text.wrappedValue
innertTextField.delegate = context.coordinator
context.coordinator.setup(innertTextField)
return innertTextField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
uiView.text = self.text.wrappedValue
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldContainer
init(_ textFieldContainer: TextFieldContainer) {
self.parent = textFieldContainer
}
func setup(_ textField:UITextField) {
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
#objc func textFieldDidChange(_ textField: UITextField) {
self.parent.text.wrappedValue = textField.text ?? ""
let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
}
Unfortunately I can't comment on ddelver's excellent answer, but I just wanted to add that for me, this did not work when I changed the bound string.
My use case is that I had a custom text field component used to edit the selected item from a list, so as you change selected item, the bound string changes. This meant that TextFieldContainer's init method was being called whenever the binding changed, but parent inside the Coordinator still referred to the initial parent.
I'm new to Swift so there may be a better fix for this, but I fixed it by adding a method to the Coordinator:
func updateParent(_ parent : TextFieldContainer) {
self.parent = parent
}
and then calling this from func updateUIView like:
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
uiView.text = self.text.wrappedValue
context.coordinator.updateParent(self)
}
You can do something like this:
final class ContentViewModel: ObservableObject {
private let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
private var realPhoneNumber = ""
#Published var formattedPhoneNumber = "" {
didSet {
let formattedText = phoneFormatter.format(formattedPhoneNumber) ?? ""
// Need this check to avoid infinite loop
if formattedPhoneNumber != formattedText {
let realText = phoneFormatter.unformat(formattedPhoneNumber) ?? ""
formattedPhoneNumber = formattedText
realPhoneNumber = realText
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
return TextField("Phone Number", text: $viewModel.formattedPhoneNumber)
}
}
The idea here is that when you manually set (assign) the text binding, the cursor of the textField moves to the end of the text.
To make UI-based editing of a NSAttributedString property (in a managed object) possible, a UITextView is used instead of a SwiftUI TextField View. The text view is located in a modal view being presented by a sheet function.
.sheet(isPresented: $presentSheet) { ...
(to illustrate and reproduce, the code below is a simplified version of this scenario)
The modal view is used to edit a selected model item that is shown in a list through a ForEach construct. The selected model item is passed as an #Observable object to the modal view.
When selecting an item "A", the modal view and the UITextView correctly shows this model item. If selecting a new item "B", the modal view correctly shows this "B" item. But if "B" is now being edited the change will affect the "A" object.
The reason for this behaviour is probably that the UIViewRepresentable view (representing the UITextView) is only initialised once. Further on from here, this seems to be caused by the way a sheet (modal) view is presented in SwiftUI (state variables are only initialised when the sheet first appear, but not the second time).
I am able to fix this malfunction by passing the selected item as a #Binding instead of an #Observable object, although I am not convinced that this is the right way to handle the situation, especially because everything works nicely, if a SwiftUI TextField is used instead of the UITextView (in the simplified case).
Worth mentioning, I seems to have figured out, what goes wrong in the case with the UITextView - without saying that this solves the problem.
In the code listed below (which repro the problem), the Coordinator's init function has one assignment that initialises the Coordinator with the parent. Since this is value and not a reference assignment, and since the Coordinator only get initialised once, an edit of the UITextView will likely access a wrong parent.
Again, I am not certain about my solution to the problem, is the right one, since everything works fine when using a SwiftUI TextField instead. I therefore hope to see some comments on this problem.
struct ContentView: View {
var states = [StringState("A"), StringState("B"), StringState("C"), StringState("D"), StringState("E")]
#State var presentSheet = false
#State var state = StringState("A")
var body: some View {
VStack {
Text("state = \(state.s)")
ForEach(states) { s in
Button(action: {
self.state = s
self.presentSheet.toggle()
})
{
Text("\(s.s)")
}
}
}
.sheet(isPresented: $presentSheet) {
EditView(state: self.state, presentSheet: self.$presentSheet)
}
}
}
struct EditView: View
{
#ObservedObject var state: StringState
#Binding var presentSheet: Bool
var body: some View {
VStack {
Text("\(state.s)")
TextView(string: $state.s) // Edit Not OK
TextField("", text: $state.s ) // Edit OK
Button(action: {
self.presentSheet.toggle()
})
{ Text("Back") }
}
}
}
struct TextView: UIViewRepresentable
{
#Binding var string: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView
{
let textview = UITextView(frame: CGRect.zero)
textview.delegate = context.coordinator
return textview
}
func updateUIView(_ uiView: UITextView, context: Context)
{
uiView.text = string
}
class Coordinator : NSObject, UITextViewDelegate
{
var parent: TextView
init(_ textView: TextView) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView)
{
self.parent.string = textView.text!
}
}
}
class StringState: Identifiable, ObservableObject
{
let ID = UUID()
var s: String
init(_ s : String) {
self.s = s
}
}
A couple of changes will fix it:
func updateUIView(_ uiView: UITextView, context: Context)
{
uiView.text = string
context.coordinator.parent = self
}
And also add #Published to your ObservableObject:
class StringState: Identifiable, ObservableObject
{
let ID = UUID()
#Published var s: String
init(_ s : String) {
self.s = s
}
}