MvvmCross 4.4.0, MvxAndroidBindingContextHelpers.Current() is Null when targeting API Level 31 and running on Android 12 - xamarin.android

Our Xamarin Android app relies heavily on some older components in MvvmCross 4.4.0 and so has not been updated to modern versions.
We used the guide found here to modernise just enough to get over the API 29/Android 10 hurdle:
https://blog.ostebaronen.dk/2020/08/old-mvvmcross-and-android-10.html
We're now at the API 31/Android 12 hurdle and, whilst everything builds and executes fine with API 31 when running on Android 11 and below, Android 12 is crashing.
The MainActivity is initialising with a null ViewModel because the LayoutInflater (already corrected for API 29) cannot retrieve the current binding context with MvxAndroidBindingContextHelpers.Current().
Nothing in the Android 12 behaviour changes guides gives an indication as to what is causing this change. We have also reviewed changes in modern versions of the MvvmCross.Binding source and can't see any obvious changes that correct this behaviour.
This is the code in LayoutInflater.cs where the problem first surfaces:
public override View? Inflate(int resource, ViewGroup? root, bool attachToRoot)
{
// Make sure our private factory is set since LayoutInflater > Honeycomb
// uses a private factory.
SetPrivateFactoryInternal();
// Save the old factory in case we are recursing because of an MvxAdapter etc.
IMvxLayoutInflaterHolderFactory? originalFactory = _bindingVisitor.Factory;
try
{
IMvxLayoutInflaterHolderFactory? factory = null;
// Get the current binding context
var currentBindingContext = MvxAndroidBindingContextHelpers.Current();
// currentBindingContext is null on Android 12, not on Android 11 and below
if (currentBindingContext != null)
{
factory = FactoryFactory?.Create(currentBindingContext.DataContext);
// Set the current factory used to generate bindings
if (factory != null)
_bindingVisitor.Factory = factory;
}
// Inflate the resource
var view = base.Inflate(resource, root, attachToRoot);
// Register bindings with clear key
if (currentBindingContext != null)
{
if (factory != null)
currentBindingContext.RegisterBindingsWithClearKey(view, factory.CreatedBindings);
}
return view;
}
finally
{
_bindingVisitor.Factory = originalFactory;
}
}
Any insight as to potential causes of the problem would be welcome, even if it's a concrete reason why MvvmCross 4.4.0 is simply no-longer viable on Android 12.
Thanks very much.
Update: We have now successfully updated to MvvmCross 5.0.1 and the problem still remains (which we expected). Our dependency is on MvvmCross.Droid.Shared, which is removed from newer versions. Can anybody recommend a guide on migrating away from MvvmCross.Droid.Shared please, as it doesn't appear to be mentioned in the upgrade guides other than to just remove it?
Update 2: Switching out the LayoutInflater for the one from MvvmCross 9.0.1 makes no difference. We've done this simply to rule that out. The issue feels like it related more to Binding.

The issue was not directly related to MvvmCross, but rather a change in Android 12 behaviour that prevented initialisation in a manner that meant ViewModel binding wasn't happening despite the rest of the code executing.
The resolution was to add some specific AndroidX initialisation lines to the AndroidManifest:
<provider android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup">
<meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup" />
</provider>
...and to
Update Xamarin.GooglePlayServices NuGet packages
Add Xamarin.AndroidX NuGet packages
Increase the minSdkVersion from 19 to 21

Related

Method ViewModel.Init receives null as parameters when linker "Link All" enabled in MvvmCross at iOS

I am facing such problem: In MvvmCross 4.4.0 on iOS project when I have changed linker mode from "Link Only SDKs" to "Link All", in the Init method of ViewModel I started to get null as a parameter of Init Method. Example below. (All other Init methods have same problem).
On PreviousViewModel:
public void RedirectToCountry()
{
/*_countryId != null */
ShowViewModel<NextViewModel>(new {countryId = _countryId});
}
On NextViewModel:
public void Init(string countryId)
{
/* countryId == null */
_countryId = countryId;
}
So, now I am looking what Method/Class/File/Namespace/Assembly do I need to save from Linker. Which component should I include?
P.S.: If someone now khow to save Prepare method from new versions of MvvmCross please answer too.
P.P.S.: I have researched that MvvmCross uses JsonSerializer for transferring data between ViewModels, tried to include assembly with serializer files (MvvmCross.Core.ViewModels) but it did not helped.
These are the assemblies I normally avoid from linking on iOS:
--linkskip=MvvmCross.Binding --linkskip=MvvmCross.Binding.iOS --linkskip=MvvmCross.Localization --linkskip=MvvmCross.Platform --linkskip=MvvmCross.Platform.iOS --linkskip=Newtonsoft.Json
You shouldn't have any MvvmCross issues when linking all assemblies after that.
So, including my iOS project helped to pass data between ViewModels.
For example:
--linkskip=GuestGuide.iOS
Also I did not included all MvvmCross assemblies excluding MvvmCross.Binding.
However it all is not understandable by me at all.

Does Enabling the Bit Code will decrease the original size of Unity IOS build

I have been going through different posts regarding Bit Code option introduced in XCode. As i am creating a project in unity trying to reduce the over the air build size. According to most of the bit code post related to unity they ending up disabling it. my questions are
If i enable the bit code for unity project i know the addition bit
code data will be stripped but does it will decrease the orignal size
of Over-the-air build as right now i have an estimate universal
Download size is 125 MB and i want it to be less then 100MB?
Can i disable the bit code for a specific framework but enable for
project?
Unity Version : 5.3.5f1t
XCode : 7.2
Thanks
As stated here
, bit code is specifically for App Store submission. And extra data will be stripped by App Store only. So I guess in all other cases like ad-hoc/OTA it will not reduce build size.
For your other question, you can use PostProcessing to change these settings.
Here is an example:
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using System.Collections;
using UnityEditor.iOS.Xcode;
using System.IO;
public class BL_BuildPostProcess
{
[PostProcessBuild]
public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
{
if (buildTarget == BuildTarget.iOS)
{
string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
PBXProject proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
string target = proj.TargetGuidByName("Unity-iPhone");
proj.SetBuildProperty(target, "ENABLE_BITCODE", "false");
File.WriteAllText(projPath, proj.WriteToString());
}
}
}
Other references:
IL2CPP Build Size Optimizations
Bitcode Support In IOS & TvOS
hope it helps :)

Using new features while supporting older iOS versions

I am developing an app using SDK 8.1, Apple LLVM 6.0 and Xcode 6.1.1. The deployment target is 6.0. I'm using NSOperationQueue and I want to use QoS whenever it's available.
The code I'm using is:
if ([self.operationQueue respondsToSelector:#selector(setQualityOfService:)]
&& (&NSOperationQualityOfServiceUserInitiated)) {
[self.operationQueue performSelector:#selector(setQualityOfService:) withObject: NSOperationQualityOfServiceUserInitiated];
} else {
//Other stuff not related to the scope of this question
}
The error I get is:
Use of undeclared identifier 'NSOperationQualityOfServiceUserInitiated'
I added the if (&NSOperationQualityOfServiceUserInitiated) part to check if this constant exists. This code worked with older versions of Xcode/Obj-C Compiler.
I am able to use selectors with performSelectorWithIdentifier but what about constants that do not have a defined value in the docs? The value of this constant is set by NSQualityOfServiceUserInitiated but there is no definition for this value that can be hardcoded.
How do I fix that?
There are several things wrong with the code.
NSOperationQualityOfServiceUserInitiated is a native type (NSInteger) so you can't use it the way that you are in either line.
The qualityOfService is a property of type NSQualityOfService. Your attempt to pass an argument to the qualityOfService method (getter method) makes no sense. If you are trying to set the quality of service, you need to call the setter but you can't use performSelector.
You want:
if ([self.operationQueue respondsToSelector:#selector(qualityOfService)]) {
self.operationQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated;
} else {
//Other stuff not related to the scope of this question
}
This code will compile fine as long as your Base SDK is iOS 8.0 or later. The Deployment Target doesn't matter.
If you also want to build this code with Xcode 5 or earlier (a Base SDK of iOS 7 or earlier) then you need to wrap the code with the proper compiler directives to check for the Base SDK.

iOS target app for both iOS 6 and iOS 7 with ECSlidingViewController component

I am using ECSlidingViewController for "hamburger" menu. I am using SDK 7.0 but I changed deployment target to iOS 6.1 and now I am trying my app with older iOS then 7. The problem is with setEdgesForExtendedLayout. There is older ECSlidingViewController for older system versions. So my question is how can I change to use old version for iOS 6.1 and older and newer version for iOS 7.0 and newer. I include files from both ECSlidingViewController projects (not by cocoapods but if it is need then it's not problem to change it). I guess I need check for OS version and then change imports but I am not sure if it is enough and what's best name convention for both project. I guess they should be in different folders (like ECSlidingViewController and ECSlidingViewControllerOld) but class should be same name, is it right?
Edit: Example of code with edgesForExtendedLayout:
- (CGRect)underLeftViewCalculatedFrameForTopViewPosition:(ECSlidingViewControllerTopViewPosition)position {
CGRect frameFromDelegate = [self frameFromDelegateForViewController:self.underLeftViewController
topViewPosition:position];
if (!CGRectIsInfinite(frameFromDelegate)) return frameFromDelegate;
CGRect containerViewFrame = self.view.bounds;
if (!(self.underLeftViewController.edgesForExtendedLayout & UIRectEdgeTop)) {
CGFloat topLayoutGuideLength = [self.topLayoutGuide length];
containerViewFrame.origin.y = topLayoutGuideLength;
containerViewFrame.size.height -= topLayoutGuideLength;
}
if (!(self.underLeftViewController.edgesForExtendedLayout & UIRectEdgeBottom)) {
CGFloat bottomLayoutGuideLength = [self.bottomLayoutGuide length];
containerViewFrame.size.height -= bottomLayoutGuideLength;
}
if (!(self.underLeftViewController.edgesForExtendedLayout & UIRectEdgeRight)) {
containerViewFrame.size.width = self.anchorRightRevealAmount;
}
return containerViewFrame;
}
I am not a fan of including duplicate versions of libraries, as this creates a big problem with naming, and a lot of work to refactor all old classes to have some *-OLD suffix. Since you have access to the source, you can modify the newer version like so:
if(NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1)
{
[vc setEdgesForExtendedLayout:UIRectEdgeNone];
//Any other iOS7-specific code.
}
First, the easiest way to do it will be have the most recent version of component that support the lowest deployment target.
But if you really want to have different version for each iOS, I don't know better solution, than just rename all classes from, for example, older version (because there was only two classes) and manage creation of this controller programmatically, because there's no way to set different class for different iOS version in xib's or storyboard's. You need to wrap each and every call of this component with iOS version check ( How to check iOS version? ).
Imports and variables you can leave for each version of component without check.
The trick you mentioned failed in this case, because it works good for different architecture, because binaries for different architectures will be included in final app and that's named fat binary, but there's the same architecture for iOS6 and iOS7 ( only one new in iOS7 - arm64). So you can't wrap only includes with preprocessor macros and get fat binary with different code for each iOS version.
I hope you've understood something from my explanation.
It seems that the only thing preventing you from using the new version with iOS 6 is the lack of topLayoutGuide and bottomLayoutGuide, which if available would just return lengths of zero anyway.
Why not "backport" those methods in your controller?
- (id<UILayoutSupport>)topLayoutGuide
{
CustomLayoutGuide * guide = [CustomLayoutGuide new];
guide.length = SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0") ? super.topLayoutGuide.length : 0.0;
return guide;
}
And the helper class
#interface CustomLayoutGuide : NSObject <UILayoutSupport>
#property(nonatomic) CGFloat length;
#end
#implementation CustomLayoutGuide
#end
IMHO the easiest thing to do (and probably the best though) is to take the current ECSlidingViewController and integrate part of the old when edgesForExtendedLayout is not available (e.g. if ([self respondsToSelector:#selector(edgesForExtendedLayout)]) { /*...*/} ). This should be pretty easy and fast to do since the old ECSlidingViewController just consists of two files and these files are also present in the new ECSlidingViewController.
You could make your life even easier when doing this by using a diff tool to be sure of what you should add.

Checking for feature / class on OS version?

I am developing an application that I want to run on both iOS4 and iOS5 but for users of iOS5 I want to use an iOS5 feature as part of the interface (iOS4 users will get something less exciting). My question is what is the procedure for checking the availability of a particular OS on a device. My understanding is that I don't check the OS version but rather the availability of a particular class, can anyone help me out of the best way to do this ...
Isn't iOS 5 under NDA?
Anyway, to check if a feature exists then try this:
if (NSClassFromString(#"UIStepper")) {
//enter code here
} else {
//enter code here
}
Customise to your needs.
EDIT: iOS 5 is now released so I can now add "UIStepper" to my code.
iOS5 is under NDA so i wouldnt mention any new classes that may or may not exist. However the following code should do what you want. It's lifted from the docs.
if ([UINewClass class]) {
// Create an instance of the class and use it.
} else {
// Alternate code path to follow when the
// class is not available.
}
This uses Weak Linking and therefore requires that the new class (UINewClass) to be in the SDK you are using to compile. It is a relatively new feature introduced in iOS 4.2 and might not be supported by all the frameworks. A workaround is to use the older style (from the same link as above):
Class cls = NSClassFromString (#"UINewClass");
if (cls) {
// Create an instance of the class and use it.
} else {
// Alternate code path to follow when the
// class is not available.
}
Class stepperClass = NSClassFromString(#"UIStepper");
if (stepperClass) {
// class is available, use it
} else {
// class not available, don't use it or use something else
}
If you're trying to get the UIStepper to gracefully degrade in 4.x, you cannot only use
if( NSClassFromString(#"UIStepper") )
Instead, you must also check for a UIStepper-specific selector having a response. As Harry Wood suggested in the comment under Bo A, a good way to do it is:
if( NSClassFromString(#"UIStepper") && [theStepper respondsToSelector:#selector(setValue:)] )
This solved the issue of my app crashing under iOS 4.x.
Harry Wood helped me solve the issue I was seeing, and I would like him to get the credit.

Resources