Add managed UIKit iOS client
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s

This commit is contained in:
2026-09-19 15:43:37 +02:00
parent d0a72176ba
commit c6715028c1
41 changed files with 1110 additions and 40 deletions
@@ -0,0 +1,67 @@
using VoiceCat.Core;
using UIKit;
namespace VoiceCat.iOS;
internal sealed class ServerListController : UITableViewController
{
private readonly AppModel model;
internal ServerListController(AppModel model) { this.model = model; Title = "Servers"; TabBarItem = new(UITabBarSystemItem.Favorites, 0); }
public override void ViewDidLoad()
{
base.ViewDidLoad(); TableView.RegisterClassForCellReuse(typeof(UITableViewCell), "server");
NavigationItem.RightBarButtonItem = new(UIBarButtonSystemItem.Add, (_, _) => PresentEditor(null)); model.Changed += Reload;
}
public override nint RowsInSection(UITableView tableView, nint section) => model.Profiles.Count;
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell("server", indexPath);
ServerProfile p = model.Profiles[indexPath.Row];
var content = cell.DefaultContentConfiguration; content.Text = p.DisplayName; content.SecondaryText = p.Authentication.ToString(); cell.ContentConfiguration = content;
cell.Accessory = UITableViewCellAccessory.DisclosureIndicator; return cell;
}
public override async void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
tableView.DeselectRow(indexPath, true); try { await model.ConnectAsync(model.Profiles[indexPath.Row]); } catch (Exception e) { UiHelpers.ShowError(this, e); }
}
public override UISwipeActionsConfiguration GetTrailingSwipeActionsConfiguration(UITableView tableView, NSIndexPath indexPath)
{
ServerProfile profile = model.Profiles[indexPath.Row];
UIContextualAction edit = UIContextualAction.FromContextualActionStyle(UIContextualActionStyle.Normal, "Edit", (_, _, done) => { PresentEditor(profile); done(true); });
UIContextualAction delete = UIContextualAction.FromContextualActionStyle(UIContextualActionStyle.Destructive, "Delete", (_, _, done) => { model.RemoveProfile(profile); done(true); });
return UISwipeActionsConfiguration.FromActions([delete, edit]);
}
private void PresentEditor(ServerProfile? profile) => PresentViewController(new UINavigationController(new ServerEditorController(model, profile)), true, null);
private void Reload() { TableView.ReloadData(); NavigationItem.Prompt = model.IsConnecting ? model.Status : null; }
}
internal sealed class ServerEditorController : UIViewController
{
private readonly AppModel model; private readonly ServerProfile? existing;
private readonly UITextField host = UiHelpers.Field("Hostname or IP address");
private readonly UITextField port = UiHelpers.Field("Port");
private readonly UISegmentedControl mode = new(["Guest", "Account"]);
private readonly UITextField name = UiHelpers.Field("Nickname or username");
private readonly UITextField password = UiHelpers.Field("Password (optional)", true);
internal ServerEditorController(AppModel model, ServerProfile? existing) { this.model = model; this.existing = existing; Title = existing is null ? "Add Server" : "Edit Server"; }
public override void ViewDidLoad()
{
base.ViewDidLoad(); View!.BackgroundColor = UIColor.SystemGroupedBackground; port.KeyboardType = UIKeyboardType.NumberPad;
UIStackView stack = new([host, port, mode, name, password]) { Axis = UILayoutConstraintAxis.Vertical, Spacing = 12, TranslatesAutoresizingMaskIntoConstraints = false };
View.AddSubview(stack); NSLayoutConstraint.ActivateConstraints([stack.TopAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.TopAnchor, 24), stack.LeadingAnchor.ConstraintEqualTo(View.LayoutMarginsGuide.LeadingAnchor), stack.TrailingAnchor.ConstraintEqualTo(View.LayoutMarginsGuide.TrailingAnchor)]);
mode.SelectedSegment = existing?.Authentication == ServerAuthentication.Account ? 1 : 0; host.Text = existing?.Host; port.Text = (existing?.Port ?? 8384).ToString(); name.Text = existing?.Username ?? existing?.Nickname;
NavigationItem.LeftBarButtonItem = new(UIBarButtonSystemItem.Cancel, (_, _) => DismissViewController(true, null));
NavigationItem.RightBarButtonItem = new(UIBarButtonSystemItem.Save, (_, _) => Save());
}
private void Save()
{
try
{
if (!ushort.TryParse(port.Text, out ushort number)) throw new ArgumentException("Enter a valid port.");
ServerAuthentication auth = mode.SelectedSegment == 1 ? ServerAuthentication.Account : ServerAuthentication.Guest;
ServerProfile p = ServerProfile.Create(host.Text ?? "", number, auth, auth == ServerAuthentication.Account ? name.Text : null, auth == ServerAuthentication.Guest ? name.Text : null, existing?.Id);
model.UpsertProfile(p, password.Text); DismissViewController(true, null);
}
catch (Exception e) { UiHelpers.ShowError(this, e); }
}
}