Android Jetpack Passing Data Between Composables - multithreading

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

Related

How do I get a function in another thread to start the next function in the main thread after it is done?

Here is, what I'm trying to do:
A Switch is turned on, starting a service in another thread (works fine so far)
When this service is successful, it should then start another function within the main thread
I don't mind whether the function is called directly by the service or the service is returning a "success"-value to the main thread, what then starts the next function from there.
Here is, what the important parts of the code looks like:
Main thread:
class SendNotif : AppCompatActivity() {
val context = this
private lateinit var Switch: Switch
// Start LocationService when the switch is on
Switch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
Toast.makeText(context, "Starting LocationService", Toast.LENGTH_SHORT).show()
Intent(applicationContext, LocationService::class.java).apply {
action = LocationService.ACTION_START
startService(this)
}
} else {
Toast.makeText(context, "Stopping LocationService", Toast.LENGTH_SHORT).show()
Intent(applicationContext, LocationService::class.java).apply {
action = LocationService.ACTION_STOP
startService(this)
}
}
}
}
fun InitiateMessage() {
// This is the function, that is supposed to start after the LocationService
}
}
This is the LocationService. After being successful, the function InitiateMessage() should start.
class LocationService: Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var locationClient: LocationClient
var lat = 0.0F
var long = 0.0F
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
locationClient = DefaultLocationClient(
applicationContext,
LocationServices.getFusedLocationProviderClient(applicationContext)
)
}
// Start or stop the service
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when(intent?.action) {
ACTION_START -> start()
ACTION_STOP -> stop()
}
return super.onStartCommand(intent, flags, startId)
}
private fun start() {
// Starting notification
val notification = NotificationCompat.Builder(this, "location")
.setContentTitle("Tracking location...")
.setContentText("Location: null")
.setSmallIcon(R.drawable.ic_launcher_background)
// Can't swipe this notification away
.setOngoing(true)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Starting the location updates
locationClient
// Every 10 seconds
.getLocationUpdates(10000L)
.catch { e -> e.printStackTrace() }
.onEach { location ->
lat = location.latitude.toString().toFloat() // .takeLast(3) // taking only the last 3 digits
long = location.longitude.toString().toFloat() // .takeLast(3)
val updatedNotification = notification.setContentText(
"Location: ($lat, $long)"
)
// notificationManager.notify(1, updatedNotification.build())
// Geofence
MyGeofence(lat, long)
}
.launchIn(serviceScope)
// startForeground(1, notification.build())
}
private fun stop() {
// Stopping the notification
stopForeground(true)
// Stopping the location service
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
companion object {
const val ACTION_START = "ACTION_START"
const val ACTION_STOP = "ACTION_STOP"
}
fun MyGeofence(lat : Float, long : Float){
val context = this
var db = DataBaseHandler(context)
var data = db.readData()
// Setting the accuracy of the geofence
val acc = 2
val safelat : Double = data.get(0).LocLat.toFloat().round(acc)
val safelong = data.get(0).LocLong.toFloat().round(acc) // .take(acc).take(acc)
val h = Handler(context.mainLooper)
if(safelat == lat.toFloat().round(acc) && safelong == long.toFloat().round(acc)){
h.post(Runnable { Toast.makeText(context, "You have reached your safe refuge! " + lat.toFloat().round(acc) + " " + long.toFloat().round(acc), Toast.LENGTH_LONG).show() })
// ToDo: Right hereafter the function InitiateMessage() should start
}
else{
h.post(Runnable { Toast.makeText(context, "You are still in great danger! " + lat.toFloat().round(acc) + " " + long.toFloat().round(acc), Toast.LENGTH_LONG).show() })
}
}
fun Float.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return round(this * multiplier) / multiplier
}
}
So far, I tried it with a Looper, which did not work.
java.lang.RuntimeException: Can't create handler inside thread Thread[DefaultDispatcher-worker-1,5,main] that has not called Looper.prepare()
But I guess the far easier way would be a returned value by the service. How do I implement this, and how do I start the next function through this returned value?
I solved my problem with an observe-function and a companion object, that is a MutableLiveData.
The companion object is placed inside the main thread:
companion object {
// var iamsafe: Boolean = false
val iamsafe: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
}
The observe-function is placed within onCreate:
val safeObserver = Observer<Boolean> { newState ->
Toast.makeText(context, "Initiating message to my mate.", Toast.LENGTH_SHORT).show()
InitiateMessage()
}
iamsafe.observe(this, safeObserver)
The companion is changed in the second thread like this:
SendNotif.iamsafe.postValue (true)

Publish background context Core Data changes in a SwiftUI view without blocking the UI

After running a background-context core data task, Xcode displays the following purple runtime warning when the updates are published in a SwiftUI view:
"[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
Besides the ContentView.swift code below, I also added container.viewContext.automaticallyMergesChangesFromParent = true to init in the default Persistence.swift code.
How can I publish the background changes on the main thread to fix the warning? (iOS 14, Swift 5)
Edit: I've changed the code below, in response to the first answer, to clarify that I'm looking for a solution that doesn't block the UI when a lot of changes are saved.
struct PersistenceHelper {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
self.context = context
}
public func fetchItem() -> [Item] {
do {
let request: NSFetchRequest<Item> = Item.fetchRequest()
var items = try self.context.fetch(request)
if items.isEmpty { // Create items if none exist
for _ in 0 ..< 250_000 {
let item = Item(context: context)
item.timestamp = Date()
item.data = "a"
}
try! context.save()
items = try self.context.fetch(request)
}
return items
} catch { assert(false) }
}
public func updateItemTimestamp(completionHandler: #escaping () -> ()) {
PersistenceController.shared.container.performBackgroundTask({ backgroundContext in
let start = Date(), request: NSFetchRequest<Item> = Item.fetchRequest()
do {
let items = try backgroundContext.fetch(request)
for item in items {
item.timestamp = Date()
item.data = item.data == "a" ? "b" : "a"
}
try backgroundContext.save() // Purple warning appears here
let interval = Double(Date().timeIntervalSince(start) * 1000) // Artificial two-second delay so cover view has time to appear
if interval < 2000 { sleep(UInt32((2000 - interval) / 1000)) }
completionHandler()
} catch { assert(false) }
})
}
}
// A cover view with an animation that shouldn't be blocked when saving the background context changes
struct CoverView: View {
#State private var toggle = true
var body: some View {
Circle()
.offset(x: toggle ? -15 : 15, y: 0)
.frame(width: 10, height: 10)
.animation(Animation.easeInOut(duration: 0.25).repeatForever(autoreverses: true))
.onAppear { toggle.toggle() }
}
}
struct ContentView: View {
#State private var items: [Item] = []
#State private var showingCoverView = false
#State private var refresh = UUID()
let persistence = PersistenceHelper()
let formatter = DateFormatter()
var didSave = NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave)
// .receive(on: DispatchQuene.main) // Doesn't help
var body: some View {
ScrollView {
LazyVStack {
Button("Update Timestamp") {
showingCoverView = true
persistence.updateItemTimestamp(completionHandler: { showingCoverView = false })
}
ForEach(items, id: \.self) { item in
Text(formatter.string(from: item.timestamp!) + " " + (item.data ?? ""))
}
}
}
.id(refresh)
.onAppear {
formatter.dateFormat = "HH:mm:ss"
items = persistence.fetchItem()
}
.onReceive(didSave) { _ in
items = persistence.fetchItem()
}
.fullScreenCover(isPresented: $showingCoverView) {
CoverView().onDisappear { refresh = UUID() }
}
}
}
Since you are performing a background task, you are on a background thread - rather than the main thread.
To switch to the main thread, change the line producing the runtime warning to the following:
DispatchQueue.main.async {
try backgroundContext.save()
}
You should use Combine and observe changes to your background context and update State values for your UI to react.
#State private var coreDataAttribute = ""
var body: some View {
Text(coreDataAttribute)
.onReceive(
CoreDataManager.shared.moc.publisher(for: \.hasChanges)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
.map{_ in CoreDataManager.shared.fetchCoreDataValue()}
.filter{$0 != coreDataAttribute}
.receive(on: DispatchQueue.main))
{ value in
coreDataAttribute = value
}
}

Action when user click on the delete button on the keyboard in SwiftUI

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

How to define multiple sharedPreferences?

I have managed to get the sharedPreferences saving values. But i don't know how to make it reference the text i am clicking on. In the // Close Alert Window section when i click ok to change the text. Ok dismisses alert dialog, then suppose to add the new price to list in sharedPreferences.
In the putString() if i use putString("Price$it", input.text.toString()).applyit doesn't appear to do anything. However if i use "Price1" any text i change is saved and upon reopening the app Price1is changed to the new price. So i know the method is working. i just have no clue how to save the particular text i am editing. I hope this makes sense. Thanks for your time.
// Created Private Price List
val sharedPreferences = getSharedPreferences("priceList", Context.MODE_PRIVATE)
//Price
(1..912).forEach {
val id = resources.getIdentifier("Price$it", "id", packageName)
val tv = findViewById<TextView>(id)
tv.text = sharedPreferences.getString("Price$it","0.00")
}
(1..912).forEach {
val id = resources.getIdentifier("Price$it", "id", packageName)
val tv = findViewById<TextView>(id)
tv.setOnLongClickListener {
//Alert Window
val alertDialog = AlertDialog.Builder(this#MainActivity).create()
alertDialog.setTitle("NEW PRICE")
val input = EditText(this#MainActivity)
//Alert Submit on Enter
input.setOnKeyListener { v, keyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
// Input changes text
tv.text = input.text
when {
tv.text.startsWith("-") -> tv.setTextColor(Color.RED)
tv.text.startsWith("+") -> tv.setTextColor(Color.GREEN)
else -> {
tv.text = "_"
tv.setTextColor(Color.DKGRAY)
}
}
// Close Alert Window
alertDialog.dismiss()
// TODO Save Price Table //THIS PART vvv
sharedPreferences.edit().putString("Price1", input.text.toString()).apply()
}
false
}
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
input.layoutParams = lp
alertDialog.setView(input)
alertDialog.show()
return#setOnLongClickListener true
}
}
You are shadowing it. In your scope you are referencing the argument of tv.setOnLongClickListener. Specify the argument name so it's not shadowed by inner lambdas.
(1..912).forEach { index ->
...
sharedPreferences.edit().putString("Price$index", input.text.toString()).apply()
}

Swift 4 - How do I test for TextView content did change?

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.

Resources