2026-09-19 15:43:37 +02:00
|
|
|
using UIKit;
|
|
|
|
|
|
|
|
|
|
namespace VoiceCat.iOS;
|
|
|
|
|
|
|
|
|
|
internal static class UiHelpers
|
|
|
|
|
{
|
|
|
|
|
internal static void ShowError(UIViewController owner, Exception exception) => ShowMessage(owner, "VoiceCat", exception.Message);
|
|
|
|
|
internal static void ShowMessage(UIViewController owner, string title, string message)
|
|
|
|
|
{
|
|
|
|
|
UIAlertController alert = UIAlertController.Create(title, message, UIAlertControllerStyle.Alert);
|
|
|
|
|
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null)); owner.PresentViewController(alert, true, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static UITextField Field(string placeholder, bool secure = false)
|
|
|
|
|
{
|
|
|
|
|
var field = new UITextField { Placeholder = placeholder, BorderStyle = UITextBorderStyle.RoundedRect,
|
|
|
|
|
SecureTextEntry = secure, TranslatesAutoresizingMaskIntoConstraints = false };
|
|
|
|
|
field.AccessibilityLabel = placeholder; return field;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-25 17:00:25 +02:00
|
|
|
|
|
|
|
|
// VoiceOver rebuilds its element tree from scratch on every UITableView.ReloadData. The model
|
|
|
|
|
// notifies far more often than any list's content actually changes - the microphone level timer
|
|
|
|
|
// alone ticks at 20 Hz - so reloading unconditionally re-announces the row under an exploring
|
|
|
|
|
// finger and destroys the element a double tap was aimed at. Reload only when the rendered text
|
|
|
|
|
// actually differs, and only while the view is on screen, so an off-screen or popped controller
|
|
|
|
|
// stops reloading instead of holding a subscription for the lifetime of the connection.
|
|
|
|
|
internal sealed class ListRefresher(UITableViewController owner, Func<string> signature, Action<Action> subscribe, Action<Action> unsubscribe)
|
|
|
|
|
{
|
|
|
|
|
// Null means nothing has been rendered yet. A signature is never null, so the first
|
|
|
|
|
// Refresh after Start or Invalidate always reloads.
|
|
|
|
|
private string? rendered;
|
|
|
|
|
private bool observing;
|
|
|
|
|
|
|
|
|
|
internal void Start()
|
|
|
|
|
{
|
|
|
|
|
if (observing) return;
|
|
|
|
|
observing = true; subscribe(Refresh); Refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void Stop()
|
|
|
|
|
{
|
|
|
|
|
if (!observing) return;
|
|
|
|
|
observing = false; unsubscribe(Refresh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forces the next Refresh to reload even when the signature is unchanged, for state the
|
|
|
|
|
// signature cannot see.
|
|
|
|
|
internal void Invalidate() => rendered = null;
|
|
|
|
|
|
|
|
|
|
internal void Refresh()
|
|
|
|
|
{
|
|
|
|
|
if (!observing || !owner.IsViewLoaded) return;
|
|
|
|
|
string next = signature();
|
|
|
|
|
if (next == rendered) return;
|
|
|
|
|
rendered = next; owner.TableView.ReloadData();
|
|
|
|
|
}
|
|
|
|
|
}
|