Dark mode not enabling/disabling when using window.overrideUserInterfaceStyle on iPad - ios

I have successfully implemented the dark mode toggle by calling window.overrideUserInterfaceStyle in my app. When running the app in real iPhone or simulator, the function works fine.
But when I run the same script in a iPad, only the view.overrideUserInterfaceStyle works. If I try to use window.overrideUserInterfaceStyle it does not update the trait collection (and neither call traitCollectionDidChange(_).
To change the style I am doing (style from white to dark):
UIApplication.shared.connectedScenes.forEach { (scene: UIScene) in
(scene.delegate as? SceneDelegate)?.window?.overrideUserInterfaceStyle = .dark //Just this one works on iPhone.
}
UIApplication.shared.delegate?.window??.overrideUserInterfaceStyle = .dark //Force
UIWindow.appearance().overrideUserInterfaceStyle = .dark //Force
When the code is executed above, it should replace the color of all UIViews that are configured with UIColor for light and dark styles. Also call traitCollectionDidChange(_). But none of these actions are happening on the iPad simulator.
This code above only works on the iPhone real/simulator, it should work on the iPad simulator too. I don't have any iPad capable of dark style here.
Maybe it's a bug on the simulator?
I also tried to create a sample app and the style change works on iPad indeed, but since it's a clean project with no libraries it should work.
I am also trying to minimize my app, still not working so I am afraid if it's a library creating a conflict.
The problem also happens when using UIApplication setup instead of UIScene.
I am using Xcode 11.3 and Swift 5 with some Cocoapod libraries.

If you are using custom transition, I recommend you to set viewController.overrideUserInterfaceStyle = .unspecified after you finish the animation.
Like this:
if #available(iOS 13.0, *) {
viewController.overrideUserInterfaceStyle = .dark
}
UIView.animate(withDuration: defaultAnimationDuration, delay: 0, animations: {
//animate viewController
}, completion: { [weak self] _ in
if #available(iOS 13.0, *) {
viewController.overrideUserInterfaceStyle = .unspecified
}
})
This will make your UIViewController follow the UIWindow user interface style after all.

Related

Enabling keyboard shortcuts on iPad for app that supports iOS 12 and 13

I have an iOS app that supports iOS 10 up-to iOS 13 and recently added Catalyst support to it. Through an extension to AppDelegate keyboard short-cuts are supported, and I would like to enable them on iPad as well.
extension AppDelegate {
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
guard builder.system == .main else { return }
// Add menus and shortcuts
}
}
This compiles fine on the Catalyst target, but when building for iOS the following error is given: 'UIMenuBuilder' is only available in iOS 13.0 or newer
The obvious solution is put an availability check in:
#available(iOS 13.0, *)
extension AppDelegate {
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
guard builder.system == .main else { return }
// Add menus and shortcuts
}
}
but then the error changes to Overriding 'buildMenu' must be as available as declaration it overrides.
So for now I excluded the extension from the build on iOS to get a working build, but that means no short-cuts on iPad.
I faced the same exact issue as you. Use:
#if targetEnvironment(macCatalyst)
around your extension instead of the #available suggestion by Xcode.

MFMailComposeViewController behaves differently in iOS 13 simulator and device

I'm trying to display MFMailComposeViewController in an app.
if MFMailComposeViewController.canSendMail() {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.navigationBar.tintColor = .white
mailComposeViewController.mailComposeDelegate = self
mailComposeViewController.setToRecipients(["support#gmail.com"])
mailComposeViewController.setSubject("Feedback")
present(mailComposeViewController, animated: true)
} else {
print("This device is not configured to send email. Please set up an email account.")
}
In iOS 12, it shows up without an issue. In both simulator and device.
But when I run the same project on a device running iOS 13, it looks like this.
The navigation bar color is gone. Also the send button is also invisible.
So I added mailComposeViewController.navigationBar.backgroundColor = .mv_primary but it still doesn't show on the device. Strangely the background color shows in the simulator.
However there's a strange behavior. The MFMailComposeViewController immediately dismisses by itself when I run it in the simulator.
The following error also shows up in the Xcode console.
[Common] [FBSSystemService][0x5f27] Error handling open request for
com.apple.MailCompositionService: {
userInfo = {
FBSOpenApplicationRequestID = 0x5f27;
}
underlyingError = ; } 2019-11-01
14:40:05.214158+0530 MailCompose[11289:262267] [Assert] Connection
request invalidated without resuming our _serviceSessionConnection.
This is an error. 2019-11-01 14:40:05.216901+0530
MailCompose[11289:262054] [General] #CompositionServices
_serviceViewControllerReady: NSError Domain=_UIViewServiceInterfaceErrorDomain Code=0
I guess the weird dismiss error is a Xcode bug. But how do I fix the background color and the send button not showing up in the device?
This is how I set all the navigationbar related styles.
UINavigationBar.appearance().barTintColor = .mv_primary
UINavigationBar.appearance().tintColor = .white
UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
if #available(iOS 11.0, *) {
UINavigationBar.appearance().largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
}
Demo project
Add this line of code before present it. It will work normal. This a change in iOS 13
mailController.modalPresentationStyle = .fullScreen
The reason why the Mail composer dismisses immediately is because you can't actually send an email from the simulator. The implementation is different from iOS itself.
What I guess is happening here is that while the simulator implementation uses just some normal UI elements, the MFMailComposeViewController on native iOS is actually hosted like UIDocumentPickerViewController or UIActivityViewController. This means screenshots and trying to traverse the view tree is impossible, because the view is not an actual part of your application. They do that because these controllers contain user private information. Hosted view controller do NOT allow for customization, and do not comply with your global UINavigationBar.appearance(). This would explain why it does show up in the simulator and not on your native device.
This is new UI Style from iOS 13. You can disable it in Storyboard or set manual.
Presenting modal in iOS 13 fullscreen

Is is possible to detect either user has chosen dark mode on iPhone from xcode 10.x

I am still using the xcode 10.2.1 and haven't upgraded to xcode 11 because of some other issues. Now I want to detect that users who are using iOS 13 has chosen dark mode or light mode as their app settings.
As per apple document, If developer build app through previous xcode the app would be in light mode by default, which is my case and is fine.
So, is there a way to detect the user current appearance mode.
There is code snippet which I am using:
if #available(iOS 13.0, *) {
guard(traitCollection.responds(to: #selector(getter: UITraitCollection.userInterfaceStyle)))
else { return }
let style = traitCollection.userInterfaceStyle
switch style {
case .light:
print("light")
case .dark:
print("dark")
case .unspecified:
print("unspecified")
#unknown default:
print("unspecified")
}
}
But it is always returning unspecified or light.
you can use this property to check if current style is dark mode or not:
if #available(iOS 13.0, *) {
if UITraitCollection.current.userInterfaceStyle == .dark {
print("Dark mode")
}
else {
print("Light mode")
}
}

Show window that covers everything when apps enters background

In iOS12 and below I used to use something similar to this to show a window on top of everything to cover my app contents. This use to work but in iOS13 betas this does not work anymore.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var coverWindow: UIWindow?
func applicationDidEnterBackground(_ application: UIApplication) {
if self.coverWindow != nil {
// Skip since cover window is already showing
return
}
let vc = UIViewController()
let label = UILabel(frame: window!.bounds)
label.text = "CoverWindow. Tap to app see contents"
vc.view = label
vc.view.backgroundColor = UIColor.lightGray
let coverWindow = UIWindow(frame: window!.bounds)
coverWindow.rootViewController = vc
coverWindow.windowLevel = .alert
coverWindow.makeKeyAndVisible()
self.coverWindow = coverWindow
}
}
Apparently window changes are not reflected in screen until app enters foreground again.
Question
Does anyone know how fix or workaround this? or maybe this approach is incorrect?
Any help would be highly appreciated
Notes
I don't use a simple view because my app might be showing other windows too and my requirement is to cover everything.
I don't use applicationWillResignActive because we want to only show coverWindow when it enters background. (TouchID authentication and other stuff might trigger applicationWillResignActive and coverWindow would incorrectly show)
Example code
Download Full working example code in Github (Run in iOS simulator 12 and 13 to see the difference)
You have to implement application life cycle, you just delete it , add those app life cycle functions and implement your codes , it ll be run without error
Answer to myself.
I reported this to Apple and it was fixed in iOS 13.1 or so. Latest version of iOS13 does NOT have this bug :)

How to make UIBarButtonItem behave consistently for light and firm touches in iOS 8.4, using Xcode 7.2/iOS 9.2

This question has been changed due to ongoing debugging. Initially the indicators were that it was iOS8.4x vs iOS9.2 on the target issue. Upon more debugging I now believe may be an issue about touch sensitivity.
I have a refresh button in UIToolBar that I disable and grey-out while refreshing as a signifier for a somewhat long refresh operation.
The code below:
was built using Xcode 7.2/iOS 9.2
works on iOS 9.x simulators for iPad2 and iPhone 5.
does not work (the button works, executes refresh logic but does not grey out during operation) on iOS8.x iPad2 and iPhone5 devices. However it works on iPhone4 iOS 8.4 devices if the button is pressed for at least 2 seconds. On a fleeting touch the method executes, but the disable action and its visual effect (grey out) do not happen.
Hypothesis: It works a simulator since it simulates a touch event through mouse clicks.
Image below shows the correct behavior during refresh on iOS9.2 simulator
Image below shows that refresh button is not greyed out during refresh on iOS 8.4 device when touched for < 1s. The target method executes, but its disable action has no effect.
As a side-note, the iPhone5 does not have SIM but is on WiFi and network functionality of the app is correct.
Do I need to do something different so that a disabled button greys-out in iOS8.x independent of touch duration? In the code, I have comments about what I have tried and did not work. Speculation: Is it possible that some artifacts of 3D touch in iOS9.2 SDK have slipped into the application binary and making it misbehave on 8.4x phone target?
import UIKit
import QuartzCore
class MyViewController: UIViewController {
let refreshButton = UIBarButtonItem(image: UIImage(named:"refresh"),landscapeImagePhone: UIImage(named:"refresh"), style: UIBarButtonItemStyle.Plain, target: nil, action: nil)
//..other buttons.
//..... Other code that has nothing to do with refreshButton
override func viewWillAppear(animated: Bool) {
refreshButton.addTargetForAction(self, action: "performRefresh:")
let space1 = UIBarButtonItem(barButtonSystemItem:UIBarButtonSystemItem.FlexibleSpace, target: nil, action: nil)
buttonArray = [someButton, space1, refreshButton] // There are more buttons.
self.setToolbarItems(buttonArray, animated: false)
} //viewWillAppear
func performRefresh(sender: UIButton) {
NSLog("************Refresh Requested")
//sender.enabled = false //compiles, OK iOS 9, but does not grey out button on iOS8
//sender.anyFunction() // Results in exception stating that sender does not know that message.
//Switching to self.refreshButton instead.
self.refreshButton.enabled = false // OK iOS 9, but does not grey out button on iOS8
//self.refreshButton.tintColor = UIColor.blueColor // has not effect in either OS
//self.refreshButton.alpha or self.refreshButton.setNeedsDisplay() are not present.
//Refresh Logic
NSLog("************Refresh Done")
self.refreshButton.enabled = true
//I had the reciprocal operations (restore tint, set alpha to 1.0 here while debugging)
} //performRefresh
} // myViewController
I might not be understanding your question so well but maybe you can just change the buttons tint color when its disabled self.refreshButton.tintColor = [UIColor lightGrayColor]; and then change the color back when its done.

Resources