PM-3350 Fix Avatar toolbar icon on extensions to load properly and to take advantage of using directly SkiaSharp to do the native conversion to UIImage. Also improved the toolbar item so that size is set appropriately.

This commit is contained in:
Federico Maccaroni 2024-01-23 17:34:27 -03:00
parent f539bf051d
commit 63904fd303
No known key found for this signature in database
GPG key ID: 5D233F8F2B034536
16 changed files with 350 additions and 400 deletions

View file

@ -0,0 +1,62 @@
using SkiaSharp;
namespace Bit.App.Controls
{
public class AvatarImageSource : StreamImageSource
{
private readonly string _text;
private readonly string _id;
private readonly string _color;
private readonly AvatarInfo _avatarInfo;
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (obj is AvatarImageSource avatar)
{
return avatar._id == _id && avatar._text == _text && avatar._color == _color;
}
return base.Equals(obj);
}
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null)
{
_id = userId;
_text = name;
if (string.IsNullOrWhiteSpace(_text))
{
_text = email;
}
_color = color;
//Workaround: [MAUI-Migration] There is currently a bug in MAUI where the actual size of the image is used instead of the size it should occupy in the Toolbar.
//This causes some issues with the position of the icon. As a workaround we make the icon smaller until this is fixed.
//Github issues: https://github.com/dotnet/maui/issues/12359 and https://github.com/dotnet/maui/pull/17120
_avatarInfo = new AvatarInfo(userId, name, email, color, DeviceInfo.Platform == DevicePlatform.iOS ? 20 : 50);
}
public override Func<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
private Task<Stream> GetStreamAsync(CancellationToken userToken = new CancellationToken())
{
var result = Draw();
return Task.FromResult(result);
}
private Stream Draw()
{
using (var img = SKAvatarImageHelper.Draw(_avatarInfo))
{
var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data?.AsStream(true);
}
}
}
}

View file

@ -0,0 +1,63 @@
using Bit.Core.Utilities;
#nullable enable
namespace Bit.App.Controls
{
public struct AvatarInfo
{
private const string DEFAULT_BACKGROUND_COLOR = "#33ffffff";
public AvatarInfo(string? userId = null, string? name = null, string? email = null, string? color = null, int size = 50)
{
Size = size;
var text = string.IsNullOrWhiteSpace(name) ? email : name;
string? upperCaseText = null;
if (string.IsNullOrEmpty(text))
{
CharsToDraw = "..";
}
else if (text.Length > 1)
{
upperCaseText = text.ToUpper();
CharsToDraw = GetFirstLetters(upperCaseText, 2);
}
else
{
CharsToDraw = upperCaseText = text.ToUpper();
}
BackgroundColor = color ?? CoreHelpers.StringToColor(userId ?? upperCaseText, DEFAULT_BACKGROUND_COLOR);
TextColor = CoreHelpers.TextColorFromBgColor(BackgroundColor);
}
public string CharsToDraw { get; }
public string BackgroundColor { get; }
public string TextColor { get; }
public int Size { get; }
private static string GetFirstLetters(string data, int charCount)
{
var sanitizedData = data.Trim();
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1 && charCount <= 2)
{
var text = string.Empty;
for (var i = 0; i < charCount; i++)
{
text += parts[i][0];
}
return text;
}
if (sanitizedData.Length > 2)
{
return sanitizedData.Substring(0, 2);
}
return sanitizedData;
}
}
}

View file

@ -0,0 +1,63 @@
using SkiaSharp;
namespace Bit.App.Controls
{
public static class SKAvatarImageHelper
{
public static SKImage Draw(AvatarInfo avatarInfo)
{
using (var bitmap = new SKBitmap(avatarInfo.Size * 2,
avatarInfo.Size * 2,
SKImageInfo.PlatformColorType,
SKAlphaType.Premul))
{
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent);
using (var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(avatarInfo.BackgroundColor)
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
using (var circlePaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(avatarInfo.BackgroundColor)
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
using (var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(avatarInfo.TextColor),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
})
{
var rect = new SKRect();
textPaint.MeasureText(avatarInfo.CharsToDraw, ref rect);
canvas.DrawText(avatarInfo.CharsToDraw, midX, midY + rect.Height / 2, textPaint);
return SKImage.FromBitmap(bitmap);
}
}
}
}
}
}
}
}

View file

@ -1,179 +0,0 @@
using Bit.Core.Utilities;
using SkiaSharp;
namespace Bit.App.Controls
{
public class AvatarImageSource : StreamImageSource
{
private readonly string _text;
private readonly string _id;
private readonly string _color;
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (obj is AvatarImageSource avatar)
{
return avatar._id == _id && avatar._text == _text && avatar._color == _color;
}
return base.Equals(obj);
}
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null)
{
_id = userId;
_text = name;
if (string.IsNullOrWhiteSpace(_text))
{
_text = email;
}
_color = color;
}
public override Func<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
private Task<Stream> GetStreamAsync(CancellationToken userToken = new CancellationToken())
{
var result = Draw();
return Task.FromResult(result);
}
private Stream Draw()
{
string chars;
string upperCaseText = null;
if (string.IsNullOrEmpty(_text))
{
chars = "..";
}
else if (_text?.Length > 1)
{
upperCaseText = _text.ToUpper();
chars = GetFirstLetters(upperCaseText, 2);
}
else
{
chars = upperCaseText = _text.ToUpper();
}
var bgColor = _color ?? CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
var size = 50;
//Workaround: [MAUI-Migration] There is currently a bug in MAUI where the actual size of the image is used instead of the size it should occupy in the Toolbar.
//This causes some issues with the position of the icon. As a workaround we make the icon smaller until this is fixed.
//Github issues: https://github.com/dotnet/maui/issues/12359 and https://github.com/dotnet/maui/pull/17120
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
size = 20;
}
using (var bitmap = new SKBitmap(size * 2,
size * 2,
SKImageInfo.PlatformColorType,
SKAlphaType.Premul))
{
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent);
using (var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor)
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
using (var circlePaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor)
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
using (var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
})
{
var rect = new SKRect();
textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
using (var img = SKImage.FromBitmap(bitmap))
{
var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data?.AsStream(true);
}
}
}
}
}
}
}
private string GetFirstLetters(string data, int charCount)
{
var sanitizedData = data.Trim();
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1 && charCount <= 2)
{
var text = string.Empty;
for (var i = 0; i < charCount; i++)
{
text += parts[i][0];
}
return text;
}
if (sanitizedData.Length > 2)
{
return sanitizedData.Substring(0, 2);
}
return sanitizedData;
}
private Color StringToColor(string str)
{
if (str == null)
{
return Color.FromArgb("#33ffffff");
}
var hash = 0;
for (var i = 0; i < str.Length; i++)
{
hash = str[i] + ((hash << 5) - hash);
}
var color = "#FF";
for (var i = 0; i < 3; i++)
{
var value = (hash >> (i * 8)) & 0xff;
var base16 = "00" + Convert.ToString(value, 16);
color += base16.Substring(base16.Length - 2);
}
return Color.FromArgb(color);
}
}
}

View file

@ -76,6 +76,7 @@
<Folder Include="Utilities\Prompts\" /> <Folder Include="Utilities\Prompts\" />
<Folder Include="Resources\Localization\" /> <Folder Include="Resources\Localization\" />
<Folder Include="Controls\Picker\" /> <Folder Include="Controls\Picker\" />
<Folder Include="Controls\Avatar\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MauiImage Include="Resources\Images\dotnet_bot.svg"> <MauiImage Include="Resources\Images\dotnet_bot.svg">
@ -105,5 +106,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Controls\Picker\" /> <None Remove="Controls\Picker\" />
<None Remove="Controls\Avatar\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,12 +2,15 @@ using System;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using MapKit;
using UIKit; using UIKit;
namespace Bit.iOS.Autofill namespace Bit.iOS.Autofill
{ {
public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController
{ {
UIBarButtonItem _cancelButton;
UIControl _accountSwitchButton;
AccountSwitchingOverlayView _accountSwitchingOverlayView; AccountSwitchingOverlayView _accountSwitchingOverlayView;
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
@ -23,22 +26,37 @@ namespace Bit.iOS.Autofill
public CredentialProviderViewController CPViewController { get; set; } public CredentialProviderViewController CPViewController { get; set; }
public override UINavigationItem BaseNavItem => NavItem; public override UINavigationItem BaseNavItem => NavItem;
public override UIBarButtonItem BaseCancelButton => CancelButton; public override UIBarButtonItem BaseCancelButton => _cancelButton;
public override UIBarButtonItem BaseSubmitButton => SubmitButton; public override UIBarButtonItem BaseSubmitButton => SubmitButton;
public override Action Success => () => CPViewController.DismissLockAndContinue(); public override Action Success => () => CPViewController.DismissLockAndContinue();
public override Action Cancel => () => CPViewController.CompleteRequest(); public override Action Cancel => () => CPViewController.CompleteRequest();
public override async void ViewDidLoad() public override async void ViewDidLoad()
{ {
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
base.ViewDidLoad(); base.ViewDidLoad();
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync();
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
{
_cancelButton,
new UIBarButtonItem(_accountSwitchButton)
}, false);
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
} }
partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) private void CancelButton_TouchUpInside(object sender, EventArgs e)
{
Cancel();
}
private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e)
{ {
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView);
} }
@ -48,9 +66,19 @@ namespace Bit.iOS.Autofill
CheckPasswordAsync().FireAndForget(); CheckPasswordAsync().FireAndForget();
} }
partial void CancelButton_Activated(UIBarButtonItem sender) protected override void Dispose(bool disposing)
{ {
Cancel(); if (disposing)
{
if (_accountSwitchButton != null)
{
_accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton);
_accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside;
}
}
base.Dispose(disposing);
} }
} }
} }

View file

@ -12,13 +12,6 @@ namespace Bit.iOS.Autofill
[Register ("LockPasswordViewController")] [Register ("LockPasswordViewController")]
partial class LockPasswordViewController partial class LockPasswordViewController
{ {
[Outlet]
UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; }
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIBarButtonItem CancelButton { get; set; }
[Outlet] [Outlet]
[GeneratedCode ("iOS Designer", "1.0")] [GeneratedCode ("iOS Designer", "1.0")]
UIKit.UITableView MainTableView { get; set; } UIKit.UITableView MainTableView { get; set; }
@ -34,27 +27,11 @@ namespace Bit.iOS.Autofill
[GeneratedCode ("iOS Designer", "1.0")] [GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIBarButtonItem SubmitButton { get; set; } UIKit.UIBarButtonItem SubmitButton { get; set; }
[Action ("AccountSwitchingBarButton_Activated:")]
partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("CancelButton_Activated:")]
partial void CancelButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("SubmitButton_Activated:")] [Action ("SubmitButton_Activated:")]
partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender);
void ReleaseDesignerOutlets () void ReleaseDesignerOutlets ()
{ {
if (AccountSwitchingBarButton != null) {
AccountSwitchingBarButton.Dispose ();
AccountSwitchingBarButton = null;
}
if (CancelButton != null) {
CancelButton.Dispose ();
CancelButton = null;
}
if (MainTableView != null) { if (MainTableView != null) {
MainTableView.Dispose (); MainTableView.Dispose ();
MainTableView = null; MainTableView = null;
@ -65,15 +42,15 @@ namespace Bit.iOS.Autofill
NavItem = null; NavItem = null;
} }
if (SubmitButton != null) {
SubmitButton.Dispose ();
SubmitButton = null;
}
if (OverlayView != null) { if (OverlayView != null) {
OverlayView.Dispose (); OverlayView.Dispose ();
OverlayView = null; OverlayView = null;
} }
if (SubmitButton != null) {
SubmitButton.Dispose ();
SubmitButton = null;
}
} }
} }
} }

View file

@ -17,6 +17,9 @@ namespace Bit.iOS.Autofill
{ {
public partial class LoginListViewController : ExtendedUIViewController public partial class LoginListViewController : ExtendedUIViewController
{ {
UIBarButtonItem _cancelButton;
UIControl _accountSwitchButton;
public LoginListViewController(IntPtr handle) public LoginListViewController(IntPtr handle)
: base(handle) : base(handle)
{ {
@ -37,12 +40,14 @@ namespace Bit.iOS.Autofill
public async override void ViewDidLoad() public async override void ViewDidLoad()
{ {
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
base.ViewDidLoad(); base.ViewDidLoad();
SubscribeSyncCompleted(); SubscribeSyncCompleted();
NavItem.Title = AppResources.Items; NavItem.Title = AppResources.Items;
CancelBarButton.Title = AppResources.Cancel; _cancelButton.Title = AppResources.Cancel;
TableView.RowHeight = UITableView.AutomaticDimension; TableView.RowHeight = UITableView.AutomaticDimension;
TableView.EstimatedRowHeight = 44; TableView.EstimatedRowHeight = 44;
@ -61,21 +66,29 @@ namespace Bit.iOS.Autofill
} }
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync();
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
{
_cancelButton,
new UIBarButtonItem(_accountSwitchButton)
}, false);
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
} }
partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) private void CancelButton_TouchUpInside(object sender, EventArgs e)
{
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView);
}
partial void CancelBarButton_Activated(UIBarButtonItem sender)
{ {
Cancel(); Cancel();
} }
private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e)
{
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView);
}
private void Cancel() private void Cancel()
{ {
CPViewController.CompleteRequest(); CPViewController.CompleteRequest();
@ -151,6 +164,21 @@ namespace Bit.iOS.Autofill
}); });
} }
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_accountSwitchButton != null)
{
_accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton);
_accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside;
}
}
base.Dispose(disposing);
}
public class TableSource : ExtensionTableSource public class TableSource : ExtensionTableSource
{ {
private LoginListViewController _controller; private LoginListViewController _controller;

View file

@ -12,17 +12,10 @@ namespace Bit.iOS.Autofill
[Register ("LoginListViewController")] [Register ("LoginListViewController")]
partial class LoginListViewController partial class LoginListViewController
{ {
[Outlet]
UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; }
[Outlet] [Outlet]
[GeneratedCode ("iOS Designer", "1.0")] [GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIBarButtonItem AddBarButton { get; set; } UIKit.UIBarButtonItem AddBarButton { get; set; }
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIBarButtonItem CancelBarButton { get; set; }
[Outlet] [Outlet]
UIKit.UIView MainView { get; set; } UIKit.UIView MainView { get; set; }
@ -36,15 +29,9 @@ namespace Bit.iOS.Autofill
[Outlet] [Outlet]
UIKit.UITableView TableView { get; set; } UIKit.UITableView TableView { get; set; }
[Action ("AccountSwitchingBarButton_Activated:")]
partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("AddBarButton_Activated:")] [Action ("AddBarButton_Activated:")]
partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("CancelBarButton_Activated:")]
partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("SearchBarButton_Activated:")] [Action ("SearchBarButton_Activated:")]
partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender);
@ -55,11 +42,6 @@ namespace Bit.iOS.Autofill
AddBarButton = null; AddBarButton = null;
} }
if (CancelBarButton != null) {
CancelBarButton.Dispose ();
CancelBarButton = null;
}
if (MainView != null) { if (MainView != null) {
MainView.Dispose (); MainView.Dispose ();
MainView = null; MainView = null;
@ -79,11 +61,6 @@ namespace Bit.iOS.Autofill
TableView.Dispose (); TableView.Dispose ();
TableView = null; TableView = null;
} }
if (AccountSwitchingBarButton != null) {
AccountSwitchingBarButton.Dispose ();
AccountSwitchingBarButton = null;
}
} }
} }
} }

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="43"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="43">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -185,20 +185,6 @@
</view> </view>
<toolbarItems/> <toolbarItems/>
<navigationItem key="navigationItem" title="Logins" id="3734"> <navigationItem key="navigationItem" title="Logins" id="3734">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="3735">
<color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<action selector="CancelBarButton_Activated:" destination="2304" id="3750"/>
</connections>
</barButtonItem>
<barButtonItem title="Account" image="person.2" catalog="system" style="plain" id="I0b-et-FGw" userLabel="Accoutn Switching Button">
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<connections>
<action selector="AccountSwitchingBarButton_Activated:" destination="2304" id="dZn-bd-bC6"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<rightBarButtonItems> <rightBarButtonItems>
<barButtonItem systemItem="add" id="3736"> <barButtonItem systemItem="add" id="3736">
<color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -216,9 +202,7 @@
</navigationItem> </navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/> <simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections> <connections>
<outlet property="AccountSwitchingBarButton" destination="I0b-et-FGw" id="KZj-EO-7wd"/>
<outlet property="AddBarButton" destination="3736" id="name-outlet-3736"/> <outlet property="AddBarButton" destination="3736" id="name-outlet-3736"/>
<outlet property="CancelBarButton" destination="3735" id="name-outlet-3735"/>
<outlet property="MainView" destination="q9o-3n-3xL" id="gjJ-12-71Q"/> <outlet property="MainView" destination="q9o-3n-3xL" id="gjJ-12-71Q"/>
<outlet property="NavItem" destination="3734" id="name-outlet-3734"/> <outlet property="NavItem" destination="3734" id="name-outlet-3734"/>
<outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/> <outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/>
@ -410,19 +394,6 @@
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" title="Verify Master Password" id="NCb-RV-Vqq"> <navigationItem key="navigationItem" title="Verify Master Password" id="NCb-RV-Vqq">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="Xoh-Zv-hhd">
<connections>
<action selector="CancelButton_Activated:" destination="cn5-F4-59n" id="1gM-mE-phn"/>
</connections>
</barButtonItem>
<barButtonItem title="Account" image="person.2" catalog="system" style="plain" id="nwd-aM-kFD" userLabel="Accoutn Switching Button">
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<connections>
<action selector="AccountSwitchingBarButton_Activated:" destination="cn5-F4-59n" id="vVZ-IM-rkU"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" title="Submit" id="gju-yD-EmI"> <barButtonItem key="rightBarButtonItem" title="Submit" id="gju-yD-EmI">
<connections> <connections>
<action selector="SubmitButton_Activated:" destination="cn5-F4-59n" id="O1U-fk-BDh"/> <action selector="SubmitButton_Activated:" destination="cn5-F4-59n" id="O1U-fk-BDh"/>
@ -430,8 +401,6 @@
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
<connections> <connections>
<outlet property="AccountSwitchingBarButton" destination="nwd-aM-kFD" id="T8F-CN-2il"/>
<outlet property="CancelButton" destination="Xoh-Zv-hhd" id="mwi-4K-maj"/>
<outlet property="MainTableView" destination="FcI-Ph-m9e" id="Ybv-5r-VGA"/> <outlet property="MainTableView" destination="FcI-Ph-m9e" id="Ybv-5r-VGA"/>
<outlet property="NavItem" destination="NCb-RV-Vqq" id="L9b-At-x0A"/> <outlet property="NavItem" destination="NCb-RV-Vqq" id="L9b-At-x0A"/>
<outlet property="OverlayView" destination="sDX-BN-qLw" id="veu-q4-CeW"/> <outlet property="OverlayView" destination="sDX-BN-qLw" id="veu-q4-CeW"/>
@ -601,13 +570,12 @@
</scene> </scene>
</scenes> </scenes>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="12959"/> <segue reference="12574"/>
<segue reference="3731"/> <segue reference="3731"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<resources> <resources>
<image name="check.png" width="90" height="90"/> <image name="check.png" width="90" height="90"/>
<image name="logo.png" width="282" height="44"/> <image name="logo.png" width="282" height="44"/>
<image name="person.2" catalog="system" width="128" height="87"/>
<systemColor name="darkTextColor"> <systemColor name="darkTextColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>

View file

@ -1,7 +1,9 @@
using Bit.App.Controls; using Bit.App.Controls;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using CoreGraphics;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using SkiaSharp.Views.iOS;
using UIKit; using UIKit;
namespace Bit.iOS.Core.Utilities namespace Bit.iOS.Core.Utilities
@ -30,12 +32,19 @@ namespace Bit.iOS.Core.Utilities
throw new NullReferenceException(nameof(_stateService)); throw new NullReferenceException(nameof(_stateService));
} }
var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(), var avatarInfo = await _stateService.GetActiveUserCustomDataAsync<AvatarInfo?>(a => a?.Profile is null
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync(), ? null
await _stateService.GetAvatarColorAsync()); : new AvatarInfo(a.Profile.UserId, a.Profile.Name, a.Profile.Email, a.Profile.AvatarColor));
using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync())
if (!avatarInfo.HasValue)
{ {
return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); return UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);
}
using (var avatarUIImage = SKAvatarImageHelper.Draw(avatarInfo.Value))
{
return avatarUIImage?.ToUIImage()?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal)
?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -100,5 +109,32 @@ namespace Bit.iOS.Core.Utilities
containerView.UserInteractionEnabled = !overlayVisible; containerView.UserInteractionEnabled = !overlayVisible;
containerView.Subviews[0].UserInteractionEnabled = !overlayVisible; containerView.Subviews[0].UserInteractionEnabled = !overlayVisible;
} }
public async Task<UIControl> CreateAccountSwitchToolbarButtonItemCustomViewAsync()
{
const float size = 40f;
var image = await CreateAvatarImageAsync();
var accountSwitchButton = new UIControl(new CGRect(0, 0, size, size));
if (image != null)
{
var accountSwitchAvatarImageView = new UIImageView(new CGRect(0, 0, size, size))
{
Image = image
};
accountSwitchButton.AddSubview(accountSwitchAvatarImageView);
}
return accountSwitchButton;
}
public void DisposeAccountSwitchToolbarButtonItemImage(UIControl accountSwitchButton)
{
if (accountSwitchButton?.Subviews?.FirstOrDefault() is UIImageView accountSwitchImageView && accountSwitchImageView.Image != null)
{
var img = accountSwitchImageView.Image;
accountSwitchImageView.Image = null;
img.Dispose();
}
}
} }
} }

View file

@ -1,47 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Services;
using Microsoft.Maui.Controls.Compatibility.Platform.iOS;
using UIKit;
namespace Bit.iOS.Core.Utilities
{
public static class ImageSourceExtensions
{
/// <summary>
/// Gets the native image from the ImageSource.
/// Taken from https://github.com/xamarin/Xamarin.Forms/blob/02dee20dfa1365d0104758e534581d1fa5958990/Xamarin.Forms.Platform.iOS/Renderers/ImageElementManager.cs#L264
/// </summary>
public static async Task<UIImage> GetNativeImageAsync(this ImageSource source, CancellationToken cancellationToken = default(CancellationToken))
{
if (source == null || source.IsEmpty)
{
return null;
}
var handler = Microsoft.Maui.Controls.Internals.Registrar.Registered.GetHandlerForObject<IImageSourceHandler>(source);
if (handler == null)
{
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed cause IImageSourceHandler couldn't be found"));
return null;
}
try
{
float scale = (float)UIScreen.MainScreen.Scale;
return await handler.LoadImageAsync(source, scale: scale, cancelationToken: cancellationToken);
}
catch (OperationCanceledException)
{
LoggerHelper.LogEvenIfCantBeResolved(new OperationCanceledException("GetNativeImageAsync was cancelled"));
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed", ex));
}
return null;
}
}
}

View file

@ -1,15 +1,17 @@
using System;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using System;
using UIKit; using UIKit;
namespace Bit.iOS.ShareExtension namespace Bit.iOS.ShareExtension
{ {
public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController
{ {
UIBarButtonItem _cancelButton;
UIControl _accountSwitchButton;
AccountSwitchingOverlayView _accountSwitchingOverlayView; AccountSwitchingOverlayView _accountSwitchingOverlayView;
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; private Lazy<AccountSwitchingOverlayHelper> _accountSwitchingOverlayHelper = new Lazy<AccountSwitchingOverlayHelper>(() => new AccountSwitchingOverlayHelper());
public LockPasswordViewController() public LockPasswordViewController()
{ {
@ -43,15 +45,33 @@ namespace Bit.iOS.ShareExtension
public override async void ViewDidLoad() public override async void ViewDidLoad()
{ {
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
base.ViewDidLoad(); base.ViewDidLoad();
_cancelButton.TintColor = ThemeHelpers.NavBarTextColor; _cancelButton.TintColor = ThemeHelpers.NavBarTextColor;
_submitButton.TintColor = ThemeHelpers.NavBarTextColor; _submitButton.TintColor = ThemeHelpers.NavBarTextColor;
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); _accountSwitchButton = await _accountSwitchingOverlayHelper.Value.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
_accountSwitchingButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(_overlayView); _navItem.SetLeftBarButtonItems(new UIBarButtonItem[]
{
_cancelButton,
new UIBarButtonItem(_accountSwitchButton)
}, false);
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.Value.CreateAccountSwitchingOverlayView(_overlayView);
}
private void CancelButton_TouchUpInside(object sender, EventArgs e)
{
Cancel();
}
private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e)
{
_accountSwitchingOverlayHelper.Value.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView);
} }
protected override void UpdateNavigationBarTheme() protected override void UpdateNavigationBarTheme()
@ -59,21 +79,11 @@ namespace Bit.iOS.ShareExtension
UpdateNavigationBarTheme(_navBar); UpdateNavigationBarTheme(_navBar);
} }
partial void AccountSwitchingButton_Activated(UIBarButtonItem sender)
{
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView);
}
partial void SubmitButton_Activated(UIBarButtonItem sender) partial void SubmitButton_Activated(UIBarButtonItem sender)
{ {
CheckPasswordAsync().FireAndForget(); CheckPasswordAsync().FireAndForget();
} }
partial void CancelButton_Activated(UIBarButtonItem sender)
{
Cancel();
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
@ -82,11 +92,11 @@ namespace Bit.iOS.ShareExtension
{ {
TableView.Source?.Dispose(); TableView.Source?.Dispose();
} }
if (_accountSwitchingButton?.Image != null) if (_accountSwitchButton != null)
{ {
var img = _accountSwitchingButton.Image; _accountSwitchingOverlayHelper.Value.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton);
_accountSwitchingButton.Image = null;
img.Dispose(); _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside;
} }
if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null) if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null)
{ {

View file

@ -12,12 +12,6 @@ namespace Bit.iOS.ShareExtension
[Register ("LockPasswordViewController")] [Register ("LockPasswordViewController")]
partial class LockPasswordViewController partial class LockPasswordViewController
{ {
[Outlet]
UIKit.UIBarButtonItem _accountSwitchingButton { get; set; }
[Outlet]
UIKit.UIBarButtonItem _cancelButton { get; set; }
[Outlet] [Outlet]
UIKit.UITableView _mainTableView { get; set; } UIKit.UITableView _mainTableView { get; set; }
@ -33,32 +27,21 @@ namespace Bit.iOS.ShareExtension
[Outlet] [Outlet]
UIKit.UIBarButtonItem _submitButton { get; set; } UIKit.UIBarButtonItem _submitButton { get; set; }
[Action ("AccountSwitchingButton_Activated:")]
partial void AccountSwitchingButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("CancelButton_Activated:")]
partial void CancelButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("SubmitButton_Activated:")] [Action ("SubmitButton_Activated:")]
partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender);
void ReleaseDesignerOutlets () void ReleaseDesignerOutlets ()
{ {
if (_accountSwitchingButton != null) {
_accountSwitchingButton.Dispose ();
_accountSwitchingButton = null;
}
if (_cancelButton != null) {
_cancelButton.Dispose ();
_cancelButton = null;
}
if (_mainTableView != null) { if (_mainTableView != null) {
_mainTableView.Dispose (); _mainTableView.Dispose ();
_mainTableView = null; _mainTableView = null;
} }
if (_navBar != null) {
_navBar.Dispose ();
_navBar = null;
}
if (_navItem != null) { if (_navItem != null) {
_navItem.Dispose (); _navItem.Dispose ();
_navItem = null; _navItem = null;
@ -73,11 +56,6 @@ namespace Bit.iOS.ShareExtension
_submitButton.Dispose (); _submitButton.Dispose ();
_submitButton = null; _submitButton = null;
} }
if (_navBar != null) {
_navBar.Dispose ();
_navBar = null;
}
} }
} }
} }

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="2vH-Do-uhk"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="2vH-Do-uhk">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -14,11 +14,11 @@
<objects> <objects>
<viewController id="bHU-LX-EpF" customClass="LoadingViewController" sceneMemberID="viewController"> <viewController id="bHU-LX-EpF" customClass="LoadingViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="z2O-Vp-jY9"> <view key="view" contentMode="scaleToFill" id="z2O-Vp-jY9">
<rect key="frame" x="0.0" y="0.0" width="414" height="808"/> <rect key="frame" x="0.0" y="0.0" width="414" height="804"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews> <subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="Zdy-yw-n0p"> <imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="Zdy-yw-n0p">
<rect key="frame" x="66" y="352" width="282" height="44"/> <rect key="frame" x="66" y="350" width="282" height="44"/>
</imageView> </imageView>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="jNx-Vd-K6U"/> <viewLayoutGuide key="safeArea" id="jNx-Vd-K6U"/>
@ -41,7 +41,7 @@
<objects> <objects>
<navigationController definesPresentationContext="YES" id="2vH-Do-uhk" customClass="ExtensionNavigationController" sceneMemberID="viewController"> <navigationController definesPresentationContext="YES" id="2vH-Do-uhk" customClass="ExtensionNavigationController" sceneMemberID="viewController">
<navigationBar key="navigationBar" hidden="YES" contentMode="scaleToFill" translucent="NO" id="JoO-jQ-16M"> <navigationBar key="navigationBar" hidden="YES" contentMode="scaleToFill" translucent="NO" id="JoO-jQ-16M">
<rect key="frame" x="0.0" y="44" width="414" height="44"/> <rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</navigationBar> </navigationBar>
<connections> <connections>
@ -61,30 +61,17 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="M1A-84-x5l"> <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="M1A-84-x5l">
<rect key="frame" x="0.0" y="88" width="414" height="774"/> <rect key="frame" x="0.0" y="92" width="414" height="770"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView> </tableView>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ijE-Pa-OBq" userLabel="OverlayView"> <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ijE-Pa-OBq" userLabel="OverlayView">
<rect key="frame" x="0.0" y="88" width="414" height="774"/> <rect key="frame" x="0.0" y="92" width="414" height="770"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view> </view>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fav-Fz-6ZK"> <navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fav-Fz-6ZK">
<rect key="frame" x="0.0" y="44" width="414" height="44"/> <rect key="frame" x="0.0" y="48" width="414" height="44"/>
<items> <items>
<navigationItem title="Verify Master Password" id="aka-In-IYk"> <navigationItem title="Verify Master Password" id="aka-In-IYk">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="LrG-Qx-w4Q">
<connections>
<action selector="CancelButton_Activated:" destination="Vi7-LV-nWW" id="qyZ-i9-Dwz"/>
</connections>
</barButtonItem>
<barButtonItem title="Item" image="person.2" catalog="system" style="plain" id="nlD-Xn-HtM" userLabel="Account Switching Button">
<color key="tintColor" systemColor="systemBackgroundColor"/>
<connections>
<action selector="AccountSwitchingButton_Activated:" destination="Vi7-LV-nWW" id="G3U-rv-UOl"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" title="Submit" id="oQD-QK-YPB"> <barButtonItem key="rightBarButtonItem" title="Submit" id="oQD-QK-YPB">
<connections> <connections>
<action selector="SubmitButton_Activated:" destination="Vi7-LV-nWW" id="DgO-TS-MPf"/> <action selector="SubmitButton_Activated:" destination="Vi7-LV-nWW" id="DgO-TS-MPf"/>
@ -116,8 +103,6 @@
</constraints> </constraints>
</view> </view>
<connections> <connections>
<outlet property="_accountSwitchingButton" destination="nlD-Xn-HtM" id="SSG-zv-bAc"/>
<outlet property="_cancelButton" destination="LrG-Qx-w4Q" id="aag-ZZ-Ifs"/>
<outlet property="_mainTableView" destination="M1A-84-x5l" id="pA4-ao-Fhu"/> <outlet property="_mainTableView" destination="M1A-84-x5l" id="pA4-ao-Fhu"/>
<outlet property="_navBar" destination="fav-Fz-6ZK" id="Q9p-Dw-ipx"/> <outlet property="_navBar" destination="fav-Fz-6ZK" id="Q9p-Dw-ipx"/>
<outlet property="_navItem" destination="aka-In-IYk" id="www-Lt-x1g"/> <outlet property="_navItem" destination="aka-In-IYk" id="www-Lt-x1g"/>
@ -132,7 +117,6 @@
</scenes> </scenes>
<resources> <resources>
<image name="logo.png" width="282" height="44"/> <image name="logo.png" width="282" height="44"/>
<image name="person.2" catalog="system" width="128" height="81"/>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>