diff --git a/src/iOS.Autofill/ListItems/HeaderItemView.cs b/src/iOS.Autofill/ListItems/HeaderItemView.cs index 4b556f02b..6f3451556 100644 --- a/src/iOS.Autofill/ListItems/HeaderItemView.cs +++ b/src/iOS.Autofill/ListItems/HeaderItemView.cs @@ -1,4 +1,5 @@ using Bit.Core.Services; +using Bit.iOS.Core.Utilities; using Foundation; using ObjCRuntime; using UIKit; @@ -27,9 +28,9 @@ namespace Bit.iOS.Autofill.ListItems { try { - _header.TextColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED); - _header.Font = UIFont.SystemFontOfSize(15); - _separator.BackgroundColor = UIColor.FromName(ColorConstants.LIGHT_SECONDARY_300); + _header.TextColor = ThemeHelpers.TextColor; + _header.Font = UIFont.SystemFontOfSize(15, UIFontWeight.Semibold); + _separator.BackgroundColor = ThemeHelpers.SeparatorColor; _header.TranslatesAutoresizingMaskIntoConstraints = false; _separator.TranslatesAutoresizingMaskIntoConstraints = false; @@ -39,14 +40,14 @@ namespace Bit.iOS.Autofill.ListItems NSLayoutConstraint.ActivateConstraints(new NSLayoutConstraint[] { - _header.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 9), - _header.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 9), - _header.TopAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TopAnchor, 3), + _header.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 9), + _header.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, 9), + _header.TopAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TopAnchor, 10), - _separator.HeightAnchor.ConstraintEqualTo(2), - _separator.TopAnchor.ConstraintEqualTo(_header.BottomAnchor, 8), - _separator.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 5), - _separator.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 5), + _separator.HeightAnchor.ConstraintEqualTo(1), + _separator.TopAnchor.ConstraintEqualTo(_header.BottomAnchor, 12), + _separator.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 7), + _separator.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -7), _separator.BottomAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.BottomAnchor, 2) }); } diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index 663963782..7c1836f8e 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -61,10 +61,12 @@ namespace Bit.iOS.Autofill NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items; _cancelButton.Title = AppResources.Cancel; - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44; TableView.BackgroundColor = ThemeHelpers.BackgroundColor; - TableView.Source = new TableSource(this); + + var tableSource = new TableSource(this); + TableView.Source = tableSource; + tableSource.RegisterTableViewCells(TableView); + if (Context.IsCreatingPasskey) { TableView.SectionHeaderHeight = 55; diff --git a/src/iOS.Autofill/LoginSearchViewController.cs b/src/iOS.Autofill/LoginSearchViewController.cs index edd1369e8..cc7380cd3 100644 --- a/src/iOS.Autofill/LoginSearchViewController.cs +++ b/src/iOS.Autofill/LoginSearchViewController.cs @@ -35,7 +35,11 @@ namespace Bit.iOS.Autofill TableView.RowHeight = UITableView.AutomaticDimension; TableView.EstimatedRowHeight = 44; - TableView.Source = new TableSource(this); + + var tableSource = new TableSource(this); + TableView.Source = tableSource; + tableSource.RegisterTableViewCells(TableView); + SearchBar.Delegate = new ExtensionSearchDelegate(TableView); await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text); } diff --git a/src/iOS.Core/Controllers/ExtendedUITableViewCell.cs b/src/iOS.Core/Controllers/ExtendedUITableViewCell.cs index 4c76d62c4..df14db57f 100644 --- a/src/iOS.Core/Controllers/ExtendedUITableViewCell.cs +++ b/src/iOS.Core/Controllers/ExtendedUITableViewCell.cs @@ -1,4 +1,7 @@ using Bit.iOS.Core.Utilities; +using CoreGraphics; +using Foundation; +using ObjCRuntime; using UIKit; namespace Bit.iOS.Core.Controllers @@ -7,15 +10,40 @@ namespace Bit.iOS.Core.Controllers { public ExtendedUITableViewCell() { - BackgroundColor = ThemeHelpers.BackgroundColor; - if (!ThemeHelpers.LightTheme) - { - SelectionStyle = UITableViewCellSelectionStyle.None; - } + ApplyTheme(); } - public ExtendedUITableViewCell(UITableViewCellStyle style, string reusedId) - : base(style, reusedId) + public ExtendedUITableViewCell(NSCoder coder) : base(coder) + { + ApplyTheme(); + } + + public ExtendedUITableViewCell(CGRect frame) : base(frame) + { + ApplyTheme(); + } + + public ExtendedUITableViewCell(UITableViewCellStyle style, string reuseIdentifier) : base(style, reuseIdentifier) + { + ApplyTheme(); + } + + public ExtendedUITableViewCell(UITableViewCellStyle style, NSString? reuseIdentifier) : base(style, reuseIdentifier) + { + ApplyTheme(); + } + + protected ExtendedUITableViewCell(NSObjectFlag t) : base(t) + { + ApplyTheme(); + } + + protected internal ExtendedUITableViewCell(NativeHandle handle) : base(handle) + { + ApplyTheme(); + } + + private void ApplyTheme() { BackgroundColor = ThemeHelpers.BackgroundColor; if (!ThemeHelpers.LightTheme) diff --git a/src/iOS.Core/Models/CipherViewModel.cs b/src/iOS.Core/Models/CipherViewModel.cs index 5a7a89789..e2f19a8e5 100644 --- a/src/iOS.Core/Models/CipherViewModel.cs +++ b/src/iOS.Core/Models/CipherViewModel.cs @@ -31,6 +31,10 @@ namespace Bit.iOS.Core.Models public CipherView CipherView { get; set; } public CipherRepromptType Reprompt { get; set; } + public bool HasFido2Credential => CipherView?.HasFido2Credential ?? false; + + public bool IsShared => CipherView?.Shared ?? false; + public class LoginUriModel { public LoginUriModel(LoginUriView data) diff --git a/src/iOS.Core/Views/CipherLoginTableViewCell.cs b/src/iOS.Core/Views/CipherLoginTableViewCell.cs new file mode 100644 index 000000000..55b01b165 --- /dev/null +++ b/src/iOS.Core/Views/CipherLoginTableViewCell.cs @@ -0,0 +1,110 @@ +using Bit.Core; +using Bit.Core.Services; +using Bit.iOS.Core.Controllers; +using Bit.iOS.Core.Utilities; +using ObjCRuntime; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public static class NSLayoutConstraintExt + { + public static NSLayoutConstraint WithId(this NSLayoutConstraint constraint, string id) + { + constraint.SetIdentifier(id); + return constraint; + } + } + + public class CipherLoginTableViewCell : ExtendedUITableViewCell + { + private readonly UILabel _title = new UILabel(); + private readonly UILabel _subtitle = new UILabel(); + private readonly UILabel _mainIcon = new UILabel(); + private readonly UILabel _orgIcon = new UILabel(); + private readonly UIView _separator = new UIView(); + + private UIStackView _mainStackView, _titleStackView; + + protected internal CipherLoginTableViewCell(NativeHandle handle) : base(handle) + { + Setup(); + } + + private void Setup() + { + try + { + _title.TextColor = ThemeHelpers.TextColor; + _title.Font = UIFont.SystemFontOfSize(14); + _title.LineBreakMode = UILineBreakMode.TailTruncation; + _title.Lines = 1; + + _subtitle.TextColor = ThemeHelpers.MutedColor; + _subtitle.Font = UIFont.SystemFontOfSize(12); + _subtitle.LineBreakMode = UILineBreakMode.TailTruncation; + _subtitle.Lines = 1; + + _mainIcon.Font = UIFont.FromName("bwi-font", 24); + _mainIcon.AdjustsFontSizeToFitWidth = true; + _mainIcon.TextColor = ThemeHelpers.PrimaryColor; + + _orgIcon.Font = UIFont.FromName("bwi-font", 15); + _orgIcon.TextColor = ThemeHelpers.MutedColor; + _orgIcon.Text = BitwardenIcons.Collection; + _orgIcon.Hidden = true; + + _separator.BackgroundColor = ThemeHelpers.SeparatorColor; + + _titleStackView = new UIStackView(new UIView[] { _title, _orgIcon }) + { + Axis = UILayoutConstraintAxis.Horizontal, + Spacing = 4 + }; + + _mainStackView = new UIStackView(new UIView[] { _titleStackView, _subtitle }) + { + Axis = UILayoutConstraintAxis.Vertical + }; + + _mainIcon.TranslatesAutoresizingMaskIntoConstraints = false; + _separator.TranslatesAutoresizingMaskIntoConstraints = false; + _mainStackView.TranslatesAutoresizingMaskIntoConstraints = false; + + ContentView.AddSubview(_mainStackView); + ContentView.AddSubview(_mainIcon); + ContentView.AddSubview(_separator); + + NSLayoutConstraint.ActivateConstraints(new NSLayoutConstraint[] + { + _mainIcon.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 9), + _mainIcon.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor), + _mainIcon.WidthAnchor.ConstraintEqualTo(31), + _mainIcon.HeightAnchor.ConstraintEqualTo(31), + + _mainStackView.LeadingAnchor.ConstraintEqualTo(_mainIcon.TrailingAnchor, 3), + _mainStackView.TopAnchor.ConstraintEqualTo(ContentView.TopAnchor, 8), + _mainStackView.TrailingAnchor.ConstraintLessThanOrEqualTo(ContentView.TrailingAnchor, 9), + + _separator.HeightAnchor.ConstraintEqualTo(1), + _separator.TopAnchor.ConstraintEqualTo(_mainStackView.BottomAnchor, 8), + _separator.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 7), + _separator.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -7), + _separator.BottomAnchor.ConstraintEqualTo(ContentView.BottomAnchor, 0) + }); + } + catch (System.Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + } + + public void SetTitle(string title) => _title.Text = title; + + public void SetSubtitle(string subtitle) => _subtitle.Text = subtitle; + + public void SetHasFido2Credential(bool has) => _mainIcon.Text = has ? BitwardenIcons.Passkey : BitwardenIcons.Globe; + + public void ShowOrganizationIcon() => _orgIcon.Hidden = false; + } +} diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs index b44c00449..f23c1df22 100644 --- a/src/iOS.Core/Views/ExtensionTableSource.cs +++ b/src/iOS.Core/Views/ExtensionTableSource.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using Bit.Core.Abstractions; +using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Resources.Localization; using Bit.Core.Utilities; @@ -13,7 +12,7 @@ namespace Bit.iOS.Core.Views { public class ExtensionTableSource : ExtendedUITableViewSource { - private const string CellIdentifier = "TableCell"; + public const string CellIdentifier = nameof(CipherLoginTableViewCell); private IEnumerable _allItems = new List(); protected ICipherService _cipherService; @@ -89,13 +88,16 @@ namespace Bit.iOS.Core.Views } } - //public IEnumerable TableItems { get; set; } - public override nint RowsInSection(UITableView tableview, nint section) { return Items == null || Items.Count() == 0 ? 1 : Items.Count(); } + public void RegisterTableViewCells(UITableView tableView) + { + tableView.RegisterClassForCellReuse(typeof(CipherLoginTableViewCell), CellIdentifier); + } + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) { if (Items == null || Items.Count() == 0) @@ -110,14 +112,9 @@ namespace Bit.iOS.Core.Views } var cell = tableView.DequeueReusableCell(CellIdentifier); - - // if there are no cells to reuse, create a new one - if (cell == null) + if (cell is null) { - Debug.WriteLine("BW Log, Make new cell for list."); - cell = new ExtendedUITableViewCell(UITableViewCellStyle.Subtitle, CellIdentifier); - cell.TextLabel.TextColor = cell.TextLabel.TintColor = ThemeHelpers.TextColor; - cell.DetailTextLabel.TextColor = cell.DetailTextLabel.TintColor = ThemeHelpers.MutedColor; + throw new InvalidOperationException($"The cell {CellIdentifier} has not been registered in the UITableView"); } return cell; } @@ -126,15 +123,35 @@ namespace Bit.iOS.Core.Views { if (Items == null || !Items.Any() - || cell?.TextLabel == null - || cell.DetailTextLabel == null) + || !(cell is CipherLoginTableViewCell cipherCell)) { return; } var item = Items.ElementAt(indexPath.Row); - cell.TextLabel.Text = item.Name; - cell.DetailTextLabel.Text = item.Username; + if (item is null) + { + return; + } + + cipherCell.SetTitle(item.Name); + cipherCell.SetSubtitle(item.Username); + cipherCell.SetHasFido2Credential(item.HasFido2Credential); + if (item.IsShared) + { + cipherCell.ShowOrganizationIcon(); + } + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + if (Items == null + || !Items.Any()) + { + return base.GetHeightForRow(tableView, indexPath); + } + + return 55; } public async Task GetTotpAsync(CipherViewModel item) diff --git a/src/iOS.Extension/LoginListViewController.cs b/src/iOS.Extension/LoginListViewController.cs index 90e08754e..cb5834a49 100644 --- a/src/iOS.Extension/LoginListViewController.cs +++ b/src/iOS.Extension/LoginListViewController.cs @@ -41,9 +41,11 @@ namespace Bit.iOS.Extension { CancelBarButton.Title = AppResources.Cancel; } - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44; - TableView.Source = new TableSource(this); + + var tableSource = new TableSource(this); + TableView.Source = tableSource; + tableSource.RegisterTableViewCells(TableView); + await ((TableSource)TableView.Source).LoadAsync(); }