Swift 4, iOS 11 - I have a UITextView that is pre-populated with text but I want users to be able to save any changes they make to the content there. I also have a Save button in the navigation bar and I would like to disable it until the user actually changes the text in the TextView.
I know how to test for empty but I don't know how to test for when the text has been edited. How do I modify the following to test for changes to the content of TextView?
#IBAction func textEditingChanged(_ sender: UITextView) {
updateSaveButtonState()
}
func updateSaveButtonState() {
let descriptionText = descriptionTextView.text ?? ""
saveButton.isEnabled = !descriptionText.isEmpty
}
We'll to use it a dynamic way and not only in single place, i tried to make it easier to implement around the whole app, subclassing the UITextView is one of the only ways we got here #holex has suggested isEdited boolean flag and it gave me an idea, Thanks to that.
Here is the steps to implement it:
First of all set the defaultText of the textView and set the target of the method that will be called when the textView will be edited, so you can customize what ever you want.
#IBOutlet weak var saveButton: UIBarButtonItem!
#IBOutlet weak var textView: SBTextView!{
didSet{
textView.defaultText = "Hello"
textView.setTarget = (selector:#selector(self.updateSaveButtonState),target:self)
}
}
Lets say you'll setup the saveButton in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// setup save button action
saveButton.action = #selector(saveAction(_:))
saveButton.target = self
self.updateSaveButtonState()
}
And last is your save action and the selector to update the view using isEdited flag.
//MARK:- Actions
#objc private func updateSaveButtonState(){
// has not been changed keep save button disabled
if self.textView.isEdited == false{
self.saveButton.isEnabled = false
self.saveButton.tintColor = .gray
}else {
// text has been changed enable save button
self.saveButton.isEnabled = true
self.saveButton.tintColor = nil // will reset the color to default
}
}
#objc private func saveAction(_ saveButton:UIBarButtonItem){
self.textView.updateDefaultText()
}
TextView Custom Class:
//
// SBTextView.swift
//
//
// Created by Saad Albasha on 11/17/17.
// Copyright © 2017 AaoIi. All rights reserved.
//
import UIKit
class SBTextView: UITextView,UITextViewDelegate {
var isEdited = false
private var selector : Selector?
private var target : UIViewController?
var setTarget: (selector:Selector?,target:UIViewController?) {
get{
return (selector,target)
}
set(newVal) {
selector = newVal.0
target = newVal.1
}
}
var textViewDefaultText = ""
var defaultText: String {
get {
return textViewDefaultText
}
set(newVal) {
textViewDefaultText = newVal
self.text = newVal
self.isEdited = false
}
}
//MARK:- Life Cycle
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
self.setupTextview()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupTextview()
}
private func setupTextview(){
// setup textview
self.text = textViewDefaultText
self.delegate = self
}
func updateDefaultText(){
self.defaultText = self.text!
// update save button state
target!.perform(self.selector, with: nil, with: nil)
}
//MARK:- Delegate
internal func textViewDidChange(_ textView: UITextView) {
if textViewDefaultText != textView.text! {
isEdited = true
}else {
isEdited = false
}
// update save button state
target!.perform(self.selector, with: nil, with: nil)
}
}
I hope this helps.
Related
I'm trying to pass a constantly updating variable "message" across my Jetpack Composables. I have a draggable box that tracks the coordinates of the box but I'm trying to send the real-time data through a TCP connection. However, I noticed that the current coordinate of the draggable box isn't passing through to the other Composable or the socket -only the same value is passed despite message changing continuously due to me dragging the box. Also, the moment dataSendButton() is pressed, the createDragImage() and its draggable box stops animating/running.
var message = "" // global Android send message
class MainActivity : ComponentActivity() {
private var textView: TextView? = null
dataSendButton()
createDragImage()
...
}
}
}
#Composable
fun createDragImage(){
val context = LocalContext.current
...
Box() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Transparent)
.size(150.dp)
.border(BorderStroke(4.dp, SolidColor(Color.Red)))
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX = someConstantX
offsetY += dragAmount.y
message = offsetY.toString()
...
#Composable
fun dataSendButton() {
val context = LocalContext.current
...
Button(
onClick = {
// **ISSUE: message in this composable is not getting updated with message value from createDragImage()
val b1 = MainActivity.TCPconnector_client(context, message)
b1.execute()
},
{
Text(text = "Send Data", color = Color.White, fontSize = 20.sp)
}
}
}
}
}
It is because that is not how you store state in Compose.
Change the declaration of the variable.
var message by mutableStateOf(...)
Then the changes to it will trigger a recomposition, and so the rest of the code should remain the same. It is always recommended to store the state holders in a viewmodel, and pass the viewmodel around instead.
This is a working code with viewmodel
class MainActivity : ComponentActivity() {
private var textView: TextView? = null
val vm by viewmodels<MViewModel>()
dataSendButton(vm.message, vm:: onMessageChange)
createDragImage(vm.message)
...
}
}
}
#Composable
fun createDragImage(message: String, onMessageChange: (String) -> Unit){
val context = LocalContext.current
...
Box() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Transparent)
.size(150.dp)
.border(BorderStroke(4.dp, SolidColor(Color.Red)))
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX = someConstantX
offsetY += dragAmount.y
onMessageChange (offsetY.toString())
...
#Composable
fun dataSendButton(message: String) {
val context = LocalContext.current
...
Button(
onClick = {
// **ISSUE: message in this composable is not getting updated with message value from createDragImage() // This seems to be an error. Calling a Composable from onClick?
val b1 = MainActivity.TCPconnector_client(context, message)
b1.execute()
},
{
Text(text = "Send Data", color = Color.White, fontSize = 20.sp)
}
}
}
}
}
class MViewModel: ViewModel(){
var message by mutableStateOf("")
private set //do not allow external modifications to ensure consistency
fun onMessageChange (newMessage: String){
message = newMessage
}
}
Note this is the ideal way of doing such implementation. However, for your specific case, if you do not need to access it anywhere else, only changing the declaration as described in the second line of the answer should do
Thanks
I try to run a function when the user click on the delete button on the keyboard when he try to modify a Textfield.
How can I do that ?
Yes it is possible, however it requires subclassing UITextField and creating your own UIViewRepresentable
This answer is based on the fantastic work done by Costantino Pistagna in his medium article but we need to do a little more work.
Firstly we need to create our subclass of UITextField, this should also conform to the UITextFieldDelegate protocol.
class WrappableTextField: UITextField, UITextFieldDelegate {
var textFieldChangedHandler: ((String)->Void)?
var onCommitHandler: (()->Void)?
var deleteHandler: (() -> Void)?
override func deleteBackward() {
super.deleteBackward()
deleteHandler?()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let currentValue = textField.text as NSString? {
let proposedValue = currentValue.replacingCharacters(in: range, with: string)
textFieldChangedHandler?(proposedValue as String)
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
onCommitHandler?()
}
}
Because we are creating our own implementation of a TextField we need three functions that we can use for callbacks.
textFieldChangeHandler this will be called when the text property updates and allows us to change the state value associated with our Textfield.
onCommitHandler this will be called when we have finished editing our TextField
deleteHandler this will be called when we perform he delete action.
The code above shows how these are used. The part that you are particularly interested in is the override func deleteBackward(), by overriding this we are able to hook into when the delete button is pressed and perform an action on it. Depending on your use case, you may want the deleteHandler to be called before you call the super.
Next we need to create our UIViewRepresentable.
struct MyTextField: UIViewRepresentable {
private let tmpView = WrappableTextField()
//var exposed to SwiftUI object init
var tag:Int = 0
var placeholder:String?
var changeHandler:((String)->Void)?
var onCommitHandler:(()->Void)?
var deleteHandler: (()->Void)?
func makeUIView(context: UIViewRepresentableContext<MyTextField>) -> WrappableTextField {
tmpView.tag = tag
tmpView.delegate = tmpView
tmpView.placeholder = placeholder
tmpView.onCommitHandler = onCommitHandler
tmpView.textFieldChangedHandler = changeHandler
tmpView.deleteHandler = deleteHandler
return tmpView
}
func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<MyTextField>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
}
This is where we create our SwiftUI version of our WrappableTextField. We create our WrappableTextField and its properties. In the makeUIView function we assign these properties. Finally in the updateUIView we set the content hugging properties, but you may choose not to do that, it really depends on your use case.
Finally we can create a small working example.
struct ContentView: View {
#State var text = ""
var body: some View {
MyTextField(tag: 0, placeholder: "Enter your name here", changeHandler: { text in
// update the state's value of text
self.text = text
}, onCommitHandler: {
// do something when the editing finishes
print("Editing ended")
}, deleteHandler: {
// do something here when you press delete
print("Delete pressed")
})
}
}
I have a textfield on my firstViewController and what I want to happen is: once the user entered a text and goes to the SecondViewController it will become the navigation bar title.
I'm new at programming and I was hoping someone could help me.
First way to navigate creating object of view controller and navigate through navigation controller
let second = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
second.title = textTitle.text!
navigationController?.pushViewController(second, animated: true)
Second way to navigate from storyboard
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard segue.identifier == "segueIdentifier" else { return }
let destination = segue.destination as? SecondViewController
destination?.strTitle = textTitle.text!
}
In second view controller add this
var strTitle : String?
override func viewDidLoad() {
super.viewDidLoad()
self.title = strTitle ?? "default string"
}
Can you try #ChanWarde way but present the view controller rather than using segues ?
let second = storyboard?.instantiateViewController(withIdentifier:
"SecondViewController") as! SecondViewController
second.title = textTitle.text!
present(second, animated: true, completion: nil)
Or try:
let second = storyboard?.instantiateViewController(withIdentifier:
"SecondViewController") as! SecondViewController
second.title = textTitle.text!
DispatchQueue.main.async(execute: {
UIApplication.shared.keyWindow?.rootViewController = second()
self.dismiss(animated: true, completion: nil)
})
I have two different pins placed on my mapview. I have an info button on each. The info buttons will segue to the a UIViewController that has a Image view (to hold a picture of the place) and a Text label ( To hold info about the place).
My problem is how can I generate the Info and picture depending on which pin annotation button was selected. The last function is the one used in order to segue to the info view controller.
class GetToTheStart: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
//map view outlet
#IBOutlet weak var mapView: MKMapView!
//defining use of location manager
let myLocMgr = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
//setting up location request
myLocMgr.desiredAccuracy = kCLLocationAccuracyBest
myLocMgr.requestWhenInUseAuthorization()
myLocMgr.startUpdatingLocation()
myLocMgr.delegate = self
mapView.delegate = self
// coordinates of desired locations for pins
var zoo1 = CLLocationCoordinate2DMake(53.347439, -6.291820)
var town1 = CLLocationCoordinate2DMake(53.347247, -6.290865)
//setting up pin 1 annotation (the zoo)
var zoopin = MKPointAnnotation()
zoopin.coordinate = zoo1
zoopin.title = "Dublin Zoo"
zoopin.subtitle = "This this the zoo"
mapView.addAnnotation(zoopin)
//setting up pin 2 annotation (the town)
var townpin = MKPointAnnotation()
townpin.coordinate = zoo1
townpin.title = "Dublin town"
townpin.subtitle = "This this the town"
mapView.addAnnotation(townpin)
}
//setting up Pin callout button for segue
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
}
let reuseIdentifier = "pin"
var pin = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseIdentifier) as? MKPinAnnotationView
if pin == nil {
pin = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
pin!.pinColor = .Red
pin!.canShowCallout = true
pin!.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
} else {
pin!.annotation = annotation
}
return pin
}
//performing segue from info button to infoViewController
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
performSegueWithIdentifier("info", sender: view)
}
For this you need to override below method. Here we will get the annotationView which will trigger the segue.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "info") {
if let annotation = sender as? MKAnnotationView {
let detailViewController = segue.destinationViewController as! DetailViewController
detailViewController.titleText = annotation.annotation?.title ?? ""
detailViewController.detaileText = annotation.annotation?.subtitle ?? ""
}
}
}
And in the detailViewController is same as your infoViewController and here I have two labels and for that I have two public variables. This is just to avoid error because at this point we don't have the label objects.
Here is the code for my DetailViewController.
import UIKit
class DetailViewController: UIViewController {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var detailLabel: UILabel!
var titleText: String? { didSet { updateUI() } }
var detaileText: String? { didSet { updateUI() } }
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
private func updateUI() {
self.titleLabel?.text = self.titleText
self.detailLabel?.text = self.detaileText
}
}
The title says it all. I'm trying to detect a tap on a pin in a MKMapView and I don't even know where to begin. Its not an UIView, so I can't add a gesture recognizer and I can't find a UIView in MKPlaceMark to add it.
You're question is not very clear but you can do like this:
import UIKit
import MapKit
protocol HandleMapSearch: class {
func dropPinZoomIn(placemark:MKPlacemark)
}
class ViewController: UIViewController {
func getDirections(){
// Here you can put anythings like:
guard let selectedPin = selectedPin else { return }
let mapItem = MKMapItem(placemark: selectedPin)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMapsWithLaunchOptions(launchOptions)
}
}
extension ViewController : MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
guard !(annotation is MKUserLocation) else { return nil }
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
pinView?.pinTintColor = UIColor.orangeColor() // The pin's color
pinView?.canShowCallout = true // To set dialogue bubbles of the pin.
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPointZero, size: smallSquare)) // To initialize the button in the dialogue bubbles of the pin.
button.addTarget(self, action: #selector(ViewController.getDirections), forControlEvents: .TouchUpInside) // To set and initialize the button.
pinView?.leftCalloutAccessoryView = button
return pinView
}
}
You can have more details in Thorn web site