mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-09 09:40:02 +01:00
PM-3349 PM-3350
Added (migrated) CustomNavigationHandler (which should partially fix the AvatarIcon in the NavBar in iOS) Added (migrated) CustomContentPageHandler (which should mostly place the AvatarIcon in the navBar in the correct place for iOS) Added Task.Delay (workaround) to allow the Avatar to load in iOS on the LoginPage Added workaround for iOS bug with the toolbar size (more info in comment in AvatarImageSource.cs) Went through the AccountViewCell MAUI-Migration comments. (and deleted/added more comments as needed) Migrated some Device calls to DeviceInfo and MainThread Added (migrated) CustomTabbedHandler (for managing the iOS TabBar)
This commit is contained in:
parent
2e4da1b87d
commit
ce9503fa0c
@ -8,7 +8,7 @@
|
|||||||
xmlns:core="clr-namespace:Bit.Core"
|
xmlns:core="clr-namespace:Bit.Core"
|
||||||
x:Name="_accountView"
|
x:Name="_accountView"
|
||||||
x:DataType="controls:AccountViewCellViewModel">
|
x:DataType="controls:AccountViewCellViewModel">
|
||||||
<!--TODO: [MAUI-Migration] add long press
|
<!--TODO: [MAUI-Migration] add long press ( https://github.com/CommunityToolkit/Maui/issues/86 )
|
||||||
xct:TouchEffect.LongPressCommand="{Binding LongPressAccountCommand, Source={x:Reference _accountView}}"
|
xct:TouchEffect.LongPressCommand="{Binding LongPressAccountCommand, Source={x:Reference _accountView}}"
|
||||||
xct:TouchEffect.LongPressCommandParameter="{Binding .}"-->
|
xct:TouchEffect.LongPressCommandParameter="{Binding .}"-->
|
||||||
<Grid RowSpacing="0"
|
<Grid RowSpacing="0"
|
||||||
@ -144,17 +144,6 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!--TODO: [MAUI-Migration] check that is the same-->
|
|
||||||
<!--<Image
|
|
||||||
Grid.Column="0"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
HorizontalOptions="Center"
|
|
||||||
Margin="14,0"
|
|
||||||
WidthRequest="{OnPlatform 24, iOS=24, Android=26}"
|
|
||||||
HeightRequest="{OnPlatform 24, iOS=24, Android=26}"
|
|
||||||
Source="plus.png"
|
|
||||||
xct:IconTintColorEffect.TintColor="{DynamicResource TextColor}"
|
|
||||||
AutomationProperties.IsInAccessibleTree="False" />-->
|
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
|
@ -79,6 +79,14 @@ namespace Bit.App.Controls
|
|||||||
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
|
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
|
||||||
var size = 50;
|
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,
|
using (var bitmap = new SKBitmap(size * 2,
|
||||||
size * 2,
|
size * 2,
|
||||||
SKImageInfo.PlatformColorType,
|
SKImageInfo.PlatformColorType,
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
using System;
|
using Bit.App.Models;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.App.Models;
|
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Maui.Controls;
|
|
||||||
using Microsoft.Maui;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@ -30,10 +26,9 @@ namespace Bit.App.Pages
|
|||||||
_vm = BindingContext as LockPageViewModel;
|
_vm = BindingContext as LockPageViewModel;
|
||||||
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
|
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
_vm.UnlockedAction = () => MainThread.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||||
|
|
||||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
|
||||||
{
|
{
|
||||||
ToolbarItems.Add(_moreItem);
|
ToolbarItems.Add(_moreItem);
|
||||||
}
|
}
|
||||||
@ -75,7 +70,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (message.Command == Constants.ClearSensitiveFields)
|
if (message.Command == Constants.ClearSensitiveFields)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(_vm.ResetPinPasswordFields);
|
MainThread.BeginInvokeOnMainThread(_vm.ResetPinPasswordFields);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (_appeared)
|
if (_appeared)
|
||||||
@ -86,6 +81,9 @@ namespace Bit.App.Pages
|
|||||||
_appeared = true;
|
_appeared = true;
|
||||||
_mainContent.Content = _mainLayout;
|
_mainContent.Content = _mainLayout;
|
||||||
|
|
||||||
|
//Workaround: This delay allows the Avatar to correctly load on iOS. The cause of this issue is also likely connected with the race conditions issue when using loading modals in iOS
|
||||||
|
await Task.Delay(50);
|
||||||
|
|
||||||
_accountAvatar?.OnAppearing();
|
_accountAvatar?.OnAppearing();
|
||||||
|
|
||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||||
@ -110,7 +108,7 @@ namespace Bit.App.Pages
|
|||||||
var tasks = Task.Run(async () =>
|
var tasks = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
Device.BeginInvokeOnMainThread(async () => await _vm.PromptBiometricAsync());
|
MainThread.BeginInvokeOnMainThread(async () => await _vm.PromptBiometricAsync());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +116,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private void PerformFocusSecretEntry(int? cursorPosition)
|
private void PerformFocusSecretEntry(int? cursorPosition)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
SecretEntry.Focus();
|
SecretEntry.Focus();
|
||||||
if (cursorPosition.HasValue)
|
if (cursorPosition.HasValue)
|
||||||
@ -153,7 +151,7 @@ namespace Bit.App.Pages
|
|||||||
var tasks = Task.Run(async () =>
|
var tasks = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await Task.Delay(50);
|
await Task.Delay(50);
|
||||||
Device.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
|
MainThread.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
src/iOS.Core/Handlers/CustomContentPageHandler.cs
Normal file
80
src/iOS.Core/Handlers/CustomContentPageHandler.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Foundation;
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
|
using UIKit;
|
||||||
|
using ContentView = Microsoft.Maui.Platform.ContentView;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Handlers
|
||||||
|
{
|
||||||
|
public partial class CustomContentPageHandler : PageHandler
|
||||||
|
{
|
||||||
|
private Page? _page;
|
||||||
|
|
||||||
|
protected override void ConnectHandler(ContentView platformView)
|
||||||
|
{
|
||||||
|
if (VirtualView is Page page)
|
||||||
|
{
|
||||||
|
_page = page;
|
||||||
|
_page.Loaded += Page_Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Page_Loaded(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
//Workaround: We can't use DisconnectHandler to dispose as we would have to call it manually from "outside" this class. So we unregister the event and set the page to null here. (it's very unlikely it would be called anyway)
|
||||||
|
if (_page != null)
|
||||||
|
{
|
||||||
|
_page.Loaded -= Page_Loaded;
|
||||||
|
_page = null;
|
||||||
|
|
||||||
|
var navController = ViewController?.NavigationController;
|
||||||
|
if (navController?.NavigationBar != null)
|
||||||
|
{
|
||||||
|
CustomizeNavBar(navController);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CustomizeNavBar(UINavigationController navigationController)
|
||||||
|
{
|
||||||
|
// Hide bottom line under nav bar
|
||||||
|
var navBar = navigationController.NavigationBar;
|
||||||
|
navBar.SetValueForKey(NSObject.FromObject(true), new Foundation.NSString("hidesShadow"));
|
||||||
|
|
||||||
|
var navigationItem = navigationController.TopViewController.NavigationItem;
|
||||||
|
var leftNativeButtons = (navigationItem.LeftBarButtonItems ?? new UIBarButtonItem[] { }).ToList();
|
||||||
|
var rightNativeButtons = (navigationItem.RightBarButtonItems ?? new UIBarButtonItem[] { }).ToList();
|
||||||
|
var newLeftButtons = new List<UIBarButtonItem>();
|
||||||
|
var newRightButtons = new List<UIBarButtonItem>();
|
||||||
|
foreach (var nativeItem in rightNativeButtons)
|
||||||
|
{
|
||||||
|
// Use reflection to get Xamarin private field "_item"
|
||||||
|
var field = nativeItem.GetType().GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (field == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(field.GetValue(nativeItem) is ToolbarItem info))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info.Priority < 0)
|
||||||
|
{
|
||||||
|
newLeftButtons.Add(nativeItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newRightButtons.Add(nativeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var nativeItem in leftNativeButtons)
|
||||||
|
{
|
||||||
|
newLeftButtons.Add(nativeItem);
|
||||||
|
}
|
||||||
|
navigationItem.RightBarButtonItems = newRightButtons.ToArray();
|
||||||
|
navigationItem.LeftBarButtonItems = newLeftButtons.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
src/iOS.Core/Handlers/CustomNavigationHandler.cs
Normal file
82
src/iOS.Core/Handlers/CustomNavigationHandler.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using Bit.App.Controls;
|
||||||
|
using CoreFoundation;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Handlers
|
||||||
|
{
|
||||||
|
//This is a Compatibility verion of the NavigationRenderer. Eventually we should see if there's a better way to implement this behavior.
|
||||||
|
public class CustomNavigationHandler : Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer
|
||||||
|
{
|
||||||
|
public override void PushViewController(UIViewController viewController, bool animated)
|
||||||
|
{
|
||||||
|
base.PushViewController(viewController, animated);
|
||||||
|
|
||||||
|
var currentPage = (Element as NavigationPage)?.CurrentPage;
|
||||||
|
if (currentPage == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var toolbarItems = currentPage.ToolbarItems;
|
||||||
|
if (!toolbarItems.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In order to get the correct index we need to do the same as XF and reverse the toolbar items list
|
||||||
|
// https://github.com/xamarin/Xamarin.Forms/blob/8f765bd87a2968bef9c86122d88c9c47be9196d2/Xamarin.Forms.Platform.iOS/Renderers/NavigationRenderer.cs#L1432
|
||||||
|
toolbarItems = toolbarItems.Where(t => t.Order != ToolbarItemOrder.Secondary)
|
||||||
|
.Reverse()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var uiBarButtonItems = TopViewController.NavigationItem.RightBarButtonItems;
|
||||||
|
if (uiBarButtonItems == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ExtendedToolbarItem toolbarItem in toolbarItems.Where(t => t is ExtendedToolbarItem eti
|
||||||
|
&&
|
||||||
|
eti.UseOriginalImage))
|
||||||
|
{
|
||||||
|
var index = toolbarItems.IndexOf(toolbarItem);
|
||||||
|
if (index < 0 || index >= uiBarButtonItems.Length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: this is awful but I can't find another way to properly prevent memory leaks from
|
||||||
|
// subscribing on the PropertyChanged event; there are several private places where Xamarin Forms
|
||||||
|
// disposes objects that are not accessible from here so I think this should cover the (un)subscription
|
||||||
|
// but we need to remember to call the internal methods of ExtendedToolbarItem on the lifecycle of the Page
|
||||||
|
toolbarItem.OnAppearingAction = () => toolbarItem.PropertyChanged += ToolbarItem_PropertyChanged;
|
||||||
|
toolbarItem.OnDisappearingAction = () => toolbarItem.PropertyChanged -= ToolbarItem_PropertyChanged;
|
||||||
|
|
||||||
|
// HACK: XF PimaryToolbarItem is sealed so we can't override it, and also it doesn't provide any
|
||||||
|
// direct way to replace it with our custom one (we can but we need to rewrite several parts of the NavigationRenderer)
|
||||||
|
// So I think this is the easiest soolution for now to set UIImageRenderingMode.AlwaysOriginal
|
||||||
|
// on the toolbar item image
|
||||||
|
void ToolbarItem_PropertyChanged(object s, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ExtendedToolbarItem.IconImageSource))
|
||||||
|
{
|
||||||
|
var uiBarButtonItem = uiBarButtonItems[index];
|
||||||
|
|
||||||
|
DispatchQueue.MainQueue.DispatchAsync(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uiBarButtonItem.Image = uiBarButtonItem.Image?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Do nothing, we can't access the proper place to properly dispose this, so here
|
||||||
|
// we can just catch and ignore the exception. This should only happen when logging out a user.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/iOS.Core/Handlers/CustomTabbedHandler.cs
Normal file
90
src/iOS.Core/Handlers/CustomTabbedHandler.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Pages;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.iOS.Core.Utilities;
|
||||||
|
using Microsoft.Maui.Controls.Handlers.Compatibility;
|
||||||
|
using Microsoft.Maui.Controls.Platform;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Handlers
|
||||||
|
{
|
||||||
|
public partial class CustomTabbedHandler : TabbedRenderer
|
||||||
|
{
|
||||||
|
private IBroadcasterService _broadcasterService;
|
||||||
|
private UITabBarItem _previousSelectedItem;
|
||||||
|
|
||||||
|
public CustomTabbedHandler()
|
||||||
|
{
|
||||||
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
|
_broadcasterService.Subscribe(nameof(CustomTabbedHandler), (message) =>
|
||||||
|
{
|
||||||
|
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||||
|
{
|
||||||
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
iOSCoreHelpers.AppearanceAdjustments();
|
||||||
|
UpdateTabBarAppearance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnElementChanged(VisualElementChangedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnElementChanged(e);
|
||||||
|
TabBar.Translucent = false;
|
||||||
|
TabBar.Opaque = true;
|
||||||
|
UpdateTabBarAppearance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ViewDidAppear(bool animated)
|
||||||
|
{
|
||||||
|
base.ViewDidAppear(animated);
|
||||||
|
|
||||||
|
if(TabBar?.Items != null)
|
||||||
|
{
|
||||||
|
if (SelectedIndex < TabBar.Items.Length)
|
||||||
|
{
|
||||||
|
_previousSelectedItem = TabBar.Items[SelectedIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ItemSelected(UITabBar tabbar, UITabBarItem item)
|
||||||
|
{
|
||||||
|
if (_previousSelectedItem == item && Element is TabsPage tabsPage)
|
||||||
|
{
|
||||||
|
tabsPage.OnPageReselected();
|
||||||
|
}
|
||||||
|
_previousSelectedItem = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_broadcasterService.Unsubscribe(nameof(CustomTabbedHandler));
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTabBarAppearance()
|
||||||
|
{
|
||||||
|
// https://developer.apple.com/forums/thread/682420
|
||||||
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
if (deviceActionService.SystemMajorVersion() >= 15)
|
||||||
|
{
|
||||||
|
var appearance = new UITabBarAppearance();
|
||||||
|
appearance.ConfigureWithOpaqueBackground();
|
||||||
|
appearance.BackgroundColor = ThemeHelpers.TabBarBackgroundColor;
|
||||||
|
appearance.StackedLayoutAppearance.Normal.IconColor = ThemeHelpers.TabBarItemColor;
|
||||||
|
appearance.StackedLayoutAppearance.Normal.TitleTextAttributes =
|
||||||
|
new UIStringAttributes { ForegroundColor = ThemeHelpers.TabBarItemColor };
|
||||||
|
TabBar.StandardAppearance = appearance;
|
||||||
|
TabBar.ScrollEdgeAppearance = TabBar.StandardAppearance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,9 @@ namespace Bit.iOS.Core.Utilities
|
|||||||
|
|
||||||
public static void ConfigureMAUIHandlers(IMauiHandlersCollection handlers)
|
public static void ConfigureMAUIHandlers(IMauiHandlersCollection handlers)
|
||||||
{
|
{
|
||||||
|
handlers.AddHandler(typeof(TabsPage), typeof(Handlers.CustomTabbedHandler));
|
||||||
|
handlers.AddHandler(typeof(NavigationPage), typeof(Handlers.CustomNavigationHandler));
|
||||||
|
handlers.AddHandler(typeof(ContentPage), typeof(Handlers.CustomContentPageHandler));
|
||||||
Handlers.ButtonHandlerMappings.Setup();
|
Handlers.ButtonHandlerMappings.Setup();
|
||||||
Handlers.DatePickerHandlerMappings.Setup();
|
Handlers.DatePickerHandlerMappings.Setup();
|
||||||
Handlers.EditorHandlerMappings.Setup();
|
Handlers.EditorHandlerMappings.Setup();
|
||||||
|
Loading…
Reference in New Issue
Block a user