The issue only happens on the iPhone XR, it works well on all other iPhone devices.
And I used the original UITabBar component, not the customized one
tabBarItem.titlePositionAdjustment.vertical = -10.0
tabBarItem.selectedImage = UIImage(named: imageName)
tabBarItem.title = barTitle
tabBarItem.image = UIImage(named: unSelectedImage)
Upate:
The issue can't be reproduced on the simulator, only on the physical device
The interesting things is, it works well on the one iPhone XR, has the issue on another iPhone XR
Update:
The user who has the issue open the Display Zoom feature
It works well when the use choose the Standard display
The solution is;
extension UIDevice {
var modelName: String {
var modelID = ""
#if targetEnvironment(simulator)
modelID = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? ""
#else
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
modelID = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
#endif
return modelID
}
}
I use the nativeScale and scale parameter to detect if the user open the Display zoom feature.
if UIScreen.main.nativeScale > UIScreen.main.scale, UIDevice.current.modelName == "iPhone11,8" {
// "iPhone11,8" for iPhone XR
// do nothing here
} else {
// for other devices
tabBarItem.titlePositionAdjustment.vertical = -10.0
}
Related
Trying to implement accompanist pager with tabs to achieve something like instagram's page displaying followers, following and subscription - 3 tab menu with pager basically. This is the code I am using.
fun UsersPager(
myDBViewModel: MyDBViewModel
) {
val tabData = listOf(
"FOLLOWING" to Icons.Filled.PermIdentity,
"ALLUSERS" to Icons.Filled.PersonOutline,
"FOLLOWERS" to Icons.Filled.PersonOutline
)
val pagerState = rememberPagerState(
0
)
val tabIndex = pagerState.currentPage
val coroutineScope = rememberCoroutineScope()
Column {
TabRow(
selectedTabIndex = tabIndex,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
tabData.forEachIndexed { index, pair ->
Tab(
selected = tabIndex == index,
onClick = {
coroutineScope.launch {
Log.d("MP18", "click on Tab num: $index")
pagerState.animateScrollToPage(index)
}
},
text = {
Text(text = pair.first)
},
icon = {
Icon(imageVector = pair.second, contentDescription = null)
})
}
}
HorizontalPager(
state = pagerState,
itemSpacing = 1.dp,
modifier = Modifier
.weight(1f),
count = tabData.size
) { index ->
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (index) {
1 -> ShowMyFollowees(myDBViewModel = myDBViewModel)
2 -> ShowMyUsers(myDBViewModel = myDBViewModel)
3 -> ShowMyFollowers(myDBViewModel = myDBViewModel)
}
}
}
}
}
Then 3 composables follow this pattern to fetch data from API and display them:
#Composable
fun ShowMyUsers(
myDBViewModel: MyDBViewModel,
) {
val pageLoadedTimes by myDBViewModel.pageLoadedTimes.observeAsState(initial = null)
val myUsersList by myDBViewModel.myUsersList.observeAsState(initial = emptyList())
val loading by myDBViewModel.loading.observeAsState(initial = myDBViewModel.loading.value)
if (myUsersList.isNullOrEmpty() && pageLoadedTimes == 0 && !loading!!) {
LaunchedEffect(key1 = Unit, block = {
Log.d("MP18", "launchedEffect in ScreenMyAccount.ShowMyUsers")
myDBViewModel.getFirstPageUsers()
})
}
ListMyUsers(myUsers = myUsersList, myDBViewModel = myDBViewModel)
}
#Composable
fun ListMyUsers(
myUsers: List<MyUser>,
myDBViewModel: MyDBViewModel
) {
val pageLoadedTimes by myDBViewModel.pageLoadedTimes.observeAsState(initial = myDBViewModel.pageLoadedTimes.value)
val loading by myDBViewModel.loading.observeAsState(initial = myDBViewModel.loading.value)
Log.d(
"MP18",
"comp ShowMyUsers and pageLoadedTimes is: $pageLoadedTimes and loading is: $loading"
)
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Red)
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp)
) {
itemsIndexed(
items = myUsers
) { index, user ->
myDBViewModel.onChangeProductScrollPosition(index)
val numRec = pageLoadedTimes?.times(PAGE_SIZE)
Log.d(
"MP188",
"in composable, page: $pageLoadedTimes, index: $index, loading: $loading, numRec: $numRec"
)
//we should query and display next page if this is true:
if ((index + 1) >= (pageLoadedTimes?.times(PAGE_SIZE)!!) && !loading!!) {
myDBViewModel.getNextPageUsers()
}
ShowSingleUser(
index = index,
pageLoadedTimes = pageLoadedTimes!!,
user = user,
myDBViewModel = myDBViewModel
)
}
}
}
}
In composables that are available, there's an API call (through ViewModel) which gets data from backend in order to populate some vars in viewModel. The problem I have is that when first tab is clicked, also the neighbouring composable gets composed and thus I'am making 2 API calls and "preparing" second tab data even if the user might never click on that tab. This is not what I want. I'd like to fetch data from tab2 and later tab3 only when there's a click on them. I hope I am clear in what's bothering me.
This is the expected behavior of the pager as the pager has been implemented by using LazyRow in accompanist pager. Basically, pager loads the second page before you scroll to it as LazyLayout is implemented in that way. If you want to cancel that you can do something like this, which I use in my code also:
// In anywhere of your composable
SideEffect {
if(currentShownItemIndex == pagerState.currentPage) {
// Make api call...
}
}
This should ensure that you are making your api call if and only if you are on the correct index
Edit: You can use Launched Effect if you want, I used SideEffect as it is easier to write and does not rely on any key and I needed a coroutine scope simply :d
Finally, this does not prevent the composition of the page in index+1 however prevents the unnecessary api call made by pager.
I found the solution for this. I added another variable in viewModel:
private val _pageInPager = MutableLiveData(0)
val pageInPager: LiveData<Int> = _pageInPager
fun setPageInPager(pageNum: Int) {
Log.d("MP188", "setPageInPager to: $pageNum")
_pageInPager.value = pageNum
}
Then in composable:
if user clicks on tab:
onClick = {
coroutineScope.launch {
Log.d("MP18", "click on Tab num: $index")
pagerState.animateScrollToPage(index)
myDBViewModel.setPageInPager(index)
}
},
or move the pager(slider):
myDBViewModel.setPageInPager(pagerState.currentPage)
I have the exact page in the variable: myDBViewModel.pageInPager, so I can add checker in LaunchedEffect before making an API call:
if (myUsersList.isNullOrEmpty() && pageLoadedTimes == 0 && !loading!! && pageInPager == 1) {
LaunchedEffect(key1 = Unit, block = {
Log.d("MP18", "launchedEffect in ScreenMyAccount.ShowMyUsers")
myDBViewModel.getFirstPageUsers()
})
I think this works ok now. Thank you #Subfly.
public useAudio(base64EncodedAudio: any, loop: boolean, volume: number) {
let _this = this;
let audioFromString = this.base64ToBuffer(base64EncodedAudio);
this._context.decodeAudioData(audioFromString, function (buffer) {
_this.audioBuffer = buffer;
_this.PlaySound(loop, volume);
}, function (error) {
TelemetryClient.error(EventType.BASE64_DECODE_ERROR, "Error decoding sound string");
});
}
private PlaySound(loop: boolean, volume: number) {
this._source = this._context.createBufferSource();
let gainNode = this._context.createGain();
gainNode.gain.value = +(volume / 100).toFixed(2);
gainNode.connect(this._context.destination);
this._source.buffer = this._audioBuffer;
this._source.loop = loop;
this._source.connect(gainNode);
this._source.start(0);
}
gainNode.gain.value doesn't seem to work properly. Value 1 and 0.15 plays the sound at the full volume.
Am I missing anything here?
The gain AudioParam can handle very large values but if you want to lower the volume its value has to be somewhere in between 0 and 1. When the value is 0 no sound will be audible and a value of 1 will actually bypass the GainNode. Any larger value will amplify the sound. But depending on the browser and operating system you may not hear that since there might be a limiter in place before the sound hits your speakers.
I have a custom UIStoryboardSegue that works as desired in iOS12.*.
One of the destination view controller is a UITabbarController: for each tab, I have a controller embedded in a navigation controller.
Unfortunately, for iOS13.*, this does not work well: the view controller lifecycle is broken, and no call the viewXXXAppear() nor the willTransition() methods are no longer issued.
It looks like makeKeyAndVisible() has no effect?!
See at the bottom how the screen UI is puzzled below without viewWillAppear() being called.
An horrible temporary workaround
I had to pull my hairs but, I have found a fix which I make public (I had to add a navigation controller on the fly).
This messes the vc hierarchy: do you have a better solution?
public class AladdinReplaceRootViewControllerSegue: UIStoryboardSegue {
override public func perform() {
guard let window = UIApplication.shared.delegate?.window as? UIWindow,
let sourceView = source.view,
let destinationView = destination.view else {
super.perform()
return
}
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
destinationView.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
window.insertSubview(destinationView, aboveSubview: sourceView)
// **My fix**
if #available(iOS 13,*) {
// I introduced an invisible navigation controller starting in iOS13 otherwise, my controller attached to the tabbar thru a navigation, dont work correctly, no viewXAppearis called.
let navigationController = UINavigationController.init(rootViewController: self.destination)
navigationController.isNavigationBarHidden = true
window.rootViewController = navigationController
}
else {
window.rootViewController = self.destination
}
window.makeKeyAndVisible()
}
}
I found a solution thanks to Unbalanced calls to begin/end appearance transitions with custom segue
What happens here is that the creation and attaching of the destination view controller happens twice, and the first one happens too soon.
So what you need to do is:
public class AladdinReplaceRootViewControllerSegue: UIStoryboardSegue {
override public func perform() {
guard let window = UIApplication.shared.delegate?.window as? UIWindow,
let sourceView = source.view,
let destinationView = destination.view else {
super.perform()
return
}
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
let mock = createMockView(view: desination.view)
window.insertSubview(mock, aboveSubview: sourceView)
//DO SOME ANIMATION HERE< MIGHT NEED TO DO mock.alpha = 0
//after the animation is done:
window.rootViewController = self.destination
mock.removeFromSuperview()
}
func createMockView(view: UIView) -> UIImageView {
UIGraphicsBeginImageContextWithOptions(view.frame.size, true, UIScreen.main.scale)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImageView(image: image)
}
}
I had a similar problem on iOS 13 when performing a custom storyboard segue that replaces the rootViewController. The original code looked like this:
#interface CustomSegue : UIStoryboardSegue
#end
#implementation CustomSegue
- (void)perform {
AppDelegate* appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
UIViewController *destination = (UIViewController *) self.destinationViewController;
[destination.view removeFromSuperview];
[appDelegate.window addSubview:destination.view];
appDelegate.window.rootViewController = destination;
}
#end
Removing the line [appDelegate.window addSubview:destination]; fixed the problem to me. Apparanently, it was unnecessary to add the new VC's view as a subview to the window. It did the job correctly even after removing that line, and it also fixed the error message "unbalanced calls to begin/end appearance transitions".
I am working on a small project and in the partial navigation view I am checking if a page is selected and highlighting the menu.
var controller = HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString().ToLower();
var home = string.Empty;
var content = string.Empty;
switch(controller) {
case "home":
home = "class=current";
break;
case "content":
content = "class=current";
break;
}
In the view I am then doing:
<li #home>Home</li>
Originally in my code I had
home = "class='current'";
Notice I had quotations around it, but when I executed the code the html source looks like
So when I remove the quatations and run it again, since it's adding them in by default, it works, even though the debugger looks like
So the project is working, my question is why is it by default adding in the quotations?
I'm not certain that MVC is adding the quotes, that is probably the Chrome DevTools doing it. If you "View page source", I don't think you will see the quotes.
Just FYI, because of these kinds of things I usually don't include the attribute in such strings, just the value...
<li class="#home">
MVC doesn't add quotes for #home. If you decompile this page, you could get codes like below:
public class _Page_Views_Home_Index_cshtml : WebViewPage<object>
{
// Methods
public override void Execute()
{
((dynamic) base.ViewBag).Title = "Home Page";
base.BeginContext("~/Views/Home/Index.cshtml", 0x27, 2, true);
this.WriteLiteral("\r\n");
base.EndContext("~/Views/Home/Index.cshtml", 0x27, 2, true);
string str = HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString();
string str2 = string.Empty;
string str3 = string.Empty;
string str4 = str;
if (str4 != null)
{
if (!(str4 == "Home"))
{
if (str4 == "content")
{
str3 = "class=current";
}
}
else
{
str2 = "class=current";
}
}
base.BeginContext("~/Views/Home/Index.cshtml", 0x1a6, 9, true);
this.WriteLiteral("\r\n\r\n<div ");
base.EndContext("~/Views/Home/Index.cshtml", 0x1a6, 9, true);
base.BeginContext("~/Views/Home/Index.cshtml", 0x1b0, 4, false);
this.Write(str2);
....
}
}
The Write methods will finally calls WebUtility.HtmlDecode method, this method replaces special chars, but will not add quotes.
Hope this helps.
I need to run test in two browsers with the same view but logged with different users. As the server is changing the cookie and logging out the first user because of the shared cookie between multiple windows in Chrome I cannot run the test. So, I wonder if it is possible to run a Chrome normal instance and an incognito one simultaneously.
Another option is to run a Chrome and a Firefox instance but I need to control what to do with each browser.
You can use two browsers. Run a script to find out which browser you are in and then have different users to log based on that.
First, get the browser, here is a script for that:
browser.getCapabilities()
.then(function(s) {
var platform = s.caps_.platform,
browserName = s.caps_.browserName,
browserVersion = s.caps_.version,
shortVersion = browserVersion.split('.')[0],
ie = /i.*explore/.test(browserName),
ff = /firefox/.test(browserName),
ch = /chrome/.test(browserName),
sa = /safari/.test(browserName),
shortName;
if (ie) {
shortName = 'ie';
} else if (ff) {
shortName = 'ff';
} else if (ch) {
shortName = 'ch';
} else if (sa) {
shortName = 'sa';
} else {
throw new Exception('Unsupported browser: '+ browserName);
}
// Returns one of these: ['ch', 'ff', 'sa', 'ie']
browser.getShortBrowserName = function() {
return shortName;
};
// Returns one of these: ['ch33', 'ff27', 'sa7', 'ie11', 'ie10', 'ie9']
browser.getShortNameVersionAll = function() {
return shortName + shortVersion;
};
// Returns one of these: ['ch', 'ff', 'sa', 'ie11', 'ie10', 'ie9']
browser.getShortNameVersion = function() {
if (ie) {
return shortName + shortVersion;
} else {
return shortName;
}
};
// Return if current browser is IE, optionally specifying if it is a particular IE version
browser.isIE = function(ver) {
if (!ver) {
return ie;
} else {
return ie && ver.toString() === shortVersion;
}
};
browser.isSafari = function() {
return sa;
};
browser.isFirefox = function() {
return ff;
};
// Return if current browser is Chrome, optionally specifying if it is a particular Chrome version
browser.isChrome = function(ver) {
if (!ver) {
return ch;
} else {
return ch && ver.toString() === shortVersion;
}
};
then you need a function to know which user to log in:
global.getUserAndPassword = function getUser() {
var rv_user = process.env.PROTRACTOR_USER;
if (browser.isFireFox() && typeof process.env.PROTRACTOR_USER_2 !== 'undefined') {
rv_user = process.env.PROTRACTOR_USER_2;
}
return [rv_user, process.env.PROTRACTOR_PASSWORD];
};
and then a login function:
global.loginFn = function loginFn() {
var user_and_pass = getUserAndPassword();
username.sendKeys(user_and_pass[0]);
password.sendKeys(user_and_pass[1]);
login.click();
};