I'm getting really close to implementing dynamic height for a UITextView in SwiftUI. Please help me work out these kinks:
The UITextView has the correct height when it appears but does not adjust height as editing is being performed; I would like it to adjust.
I'm receiving this in the console every time I edit the text in the TextView: [SwiftUI] Modifying state during view update, this will cause undefined behavior.
Here's my code:
ItemEditView
TextView(text: $notes, textViewHeight: $textViewHeight)
.frame(height: self.textViewHeight)
UITextView
import SwiftUI
struct TextView: UIViewRepresentable {
#Binding var text: String
#Binding var textViewHeight: CGFloat
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = .systemFont(ofSize: 17)
textView.backgroundColor = .clear
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
class Coordinator: NSObject, UITextViewDelegate {
var control: TextView
init(_ control: TextView) {
self.control = control
}
func textViewDidChange(_ textView: UITextView) {
control.text = textView.text
}
}
func makeCoordinator() -> TextView.Coordinator {
Coordinator(self)
}
}
Similar questions have been asked, but none of the solutions worked for me.
Related
I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable. I am primarily using SwiftUI but am using UIKit here as it seems SwiftUI does not currently support the functionality needed.
My problem is, when I set the NSMutableAttributedString to be already containing a string with attributes, if I then select that text in the UIViewRepresentable before typing any new text and press the underline button in the UIToolBar to add the attribute, the attribute is added to the NSMutableAttributedString but the UIView does not update to show the updated NSMutableAttributedString. However, if I type a single character and then select the text and add the underline attribute, the UIView updates.
Could someone explain why this is and maybe point me towards a solution? Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the string before typing anything new",
attributes: [.foregroundColor: UIColor.blue])
var body: some View {
EditorExample(outerMutableString: $mutableAttributedString)
}
}
struct EditorExample: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
#State private var outerSelectedRange: NSRange = NSRange()
func makeUIView(context: Context) -> some UITextView {
// make UITextView
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = context.coordinator
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: context.coordinator,
action: #selector(context.coordinator.underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.attributedText = outerMutableString
}
func makeCoordinator() -> Coordinator {
Coordinator(innerMutableString: $outerMutableString, selectedRange: $outerSelectedRange)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var innerMutableString: NSMutableAttributedString
#Binding var selectedRange: NSRange
init(innerMutableString: Binding<NSMutableAttributedString>, selectedRange: Binding<NSRange>) {
self._innerMutableString = innerMutableString
self._selectedRange = selectedRange
}
func textViewDidChange(_ textView: UITextView) {
innerMutableString = textView.textStorage
}
func textViewDidChangeSelection(_ textView: UITextView) {
selectedRange = textView.selectedRange
}
#objc func underline() {
if (selectedRange.length > 0) {
innerMutableString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: selectedRange)
}
}
}
}
It's not working because NSAttributedString is a class and #State is for value types like structs. This means the dependency tracking is broken and things won't update correctly.
Also your UIViewRepresentable and Coordinator design is non-standard so I thought I would share an example of the correct way to do it. The binding is change to a string, which is a value type so it's working (minus the underline feature obviously).
struct ContentView: View {
//#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
// string: "this is the string before typing anything new",
// attributes: [.foregroundColor: UIColor.blue])
#State var string = "this is the string before typing anything new"
var body: some View {
VStack {
// EditorExample(outerMutableString: $mutableAttributedString)
// EditorExample(outerMutableString: $mutableAttributedString) // a second to test bindings are working\
//Text(mutableAttributedString.string)
EditorExample(outerMutableString2: $string)
EditorExample(outerMutableString2: $string)
}
}
}
struct EditorExample: UIViewRepresentable {
//#Binding var outerMutableString: NSMutableAttributedString
#Binding var outerMutableString2: String
// this is called first
func makeCoordinator() -> Coordinator {
// we can't pass in any values to the Coordinator because they will be out of date when update is called the second time.
Coordinator()
}
// this is called second
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
// this is called third and then repeatedly every time a let or `#Binding var` that is passed to this struct's init has changed from last time.
func updateUIView(_ uiView: UITextView, context: Context) {
//uiView.attributedText = outerMutableString
uiView.text = outerMutableString2
// we don't usually pass bindings in to the coordinator and instead use closures.
// we have to set a new closure because the binding might be different.
context.coordinator.stringDidChange2 = { string in
outerMutableString2 = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = self
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: self,
action: #selector(underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}()
//var stringDidChange: ((NSMutableAttributedString) -> ())?
var stringDidChange2: ((String) -> ())?
func textViewDidChange(_ textView: UITextView) {
//innerMutableString = textView.textStorage
//stringDidChange?(textView.textStorage)
stringDidChange2?(textView.text)
}
func textViewDidChangeSelection(_ textView: UITextView) {
// selectedRange = textView.selectedRange
}
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
textView.textStorage.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
// stringDidChange?(textView.textStorage)
}
}
}
}
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
}
Using Swift 5.5, Xcode 13.0, iOS 15.0.1,
Since Apple does not a good job on ScrollViews for SwiftUI (yet?), I had to implement my own zoomable ScrollView. See code below.
I had it successfully running for iOS13 and iOS14 - but now updating to iOS 15.0.1 I had to realise that it no longer works !
The error message is as follows:
objc[10882]: Cannot form weak reference to instance (0x1078d5540) of class _TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_5ImageVS_18_AspectRatioLayout__. It is possible that this object was over-released, or is in the process of deallocation.
dyld4 config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
(lldb)
I have everything inside a SwiftUIPager, and the problem occurs on the very last page when swiping. However, I don't think it is the Pagers fault since I can replace the ZoomableScrolView by a normal ImageView and everything works. Therefore I think Apple messed something up in their UIViewRepresentable. But maybe you can tell me more ??
Here is the entire code of my ZoomableScrolView:
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
#Binding var didZoom: Bool
private var content: Content
init(didZoom: Binding<Bool>, #ViewBuilder content: () -> Content) {
_didZoom = didZoom
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), didZoom: $didZoom)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var didZoom: Bool
init(hostingController: UIHostingController<Content>, didZoom: Binding<Bool>) {
self.hostingController = hostingController
_didZoom = didZoom
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
didZoom = !(scrollView.zoomScale == scrollView.minimumZoomScale)
}
}
}
I finally found a workaround.
Add the following code inside the ZoomableScrollView-UIViewRepresentable class:
static func dismantleUIView(_ uiView: UIScrollView, coordinator: Coordinator) {
uiView.delegate = nil
coordinator.hostingController.view = nil
}
I'm stuck with the really strange problem. I'm implementing map into my SwiftUI app. It should act like a normal map (drag, scroll and so on). When changing position (that is binding point) the app gets an address via geocoder.
Also user can click "Change" button and enter address manually (with autocompletion). After selecting the address, the map should move to the reverse geocoded point.
Built-in SwiftUI Map() is a good thing, but... it's unreal to make it show buildings. And in the app it's something that matters. So, going with UIViewRepresentable gives me another strange problem.
If I set the center coordinate in UpdateUIView, the map stops any interactivity. Otherwise changing the address manually doesn't work.
What could be wrong with this?
struct MapView: UIViewRepresentable {
#Binding var point: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.showsBuildings = true
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// uiView.setCenter(point, animated: true)
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
self.parent.point = mapView.centerCoordinate
}
}
}
I tried wrapping everything into DispatchQueue.main.async {} - not working (and honestly I don't think it could)
I also tried this solution, but it worked neither: https://www.reddit.com/r/SwiftUI/comments/kti9r9/uiviewrepresentable_how_to_update_bindings/
I also had the same problem. I solved this using #state. So every time the mapView changes, the corresponding function of the coordinator is definitely called. Hope it helps.
struct YourView: View {
#State mapView: MKMapView = .init()
#State var point: CLLocationCoordinate2D = [...]
var body: some View {
MapView(mapView: $mapView, point: $point)
...
}
struct MapView: UIViewRepresentable {
#Binding var mapView: MKMapView
#Binding var point: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
self.mapView = MKMapView()
self.mapView.showsBuildings = true
self.mapView.delegate = context.coordinator
return self.mapView
}
...
func updateUIView(_ uiView: MKMapView, context: Context) {
// uiView.setCenter(point, animated: true)
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
self.parent.point = self.mapView.centerCoordinate
}
}
}
I have created a SwiftUI TextView based on a UITextView using UIViewRepresentable (s. code below). Displaying text in Swiftui works OK.
But now I need to access internal functions of UITextView from my model. How do I call e.g. UITextView.scrollRangeToVisible(_:) or access properties like UITextView.isEditable ?
My model needs to do these modifications based on internal model states.
Any ideas ? Thanks
(p.s. I am aware of TextEditor in SwiftUI, but I need support for iOS 13!)
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}
You can use something like configurator callback pattern, like
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
var configurator: ((UITextView) -> ())? // << here !!
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
// alternat is to call this function in makeUIView, which is called once,
// and the store externally to send methods directly.
configurator?(myTextView) // << here !!
}
// ... other code
}
and use it in your SwiftUI view like
TextView(...) { uiText in
uiText.isEditing = some
}
Note: depending on your scenarios it might be additional conditions need to avoid update cycling, not sure.