Disabling Dynamic Type in Swift - ios

I have a Sprite Kit based game that uses a UIView within one of the scenes, and I do that so that I can take advantage of the UITableViewController to present a game settings screen.
The difficulty I am running into is that when a user sets their iPad system accessibility settings to use (extra) large type, the text within the UITableView is too large for the cells and it looks just plain silly.
What I would like to do is straight up disable the dynamic type within the app so it always displays the same sized type in the cells.
I have found another similar posting (here) but the response offers an Objective-C response:
#import <objc/runtime.h>
#implementation AppDelegate
NSString* swizzled_preferredContentSizeCategory(id self, SEL _cmd) {
return UIContentSizeCategoryLarge; // Set category you prefer, Large being iOS' default.
}
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
Method method = class_getInstanceMethod([UIApplication class], #selector(preferredContentSizeCategory));
method_setImplementation(method, (IMP)swizzled_preferredContentSizeCategory);
...
}
I need to do this in Swift.
What is the correct way to do this same thing in Swift in Xcode 7+ ?

Thanks #zeeple for the solution.
Here is the answer to the original question:
"preferredContentSizeCategory" in Objective-C is a method, but in Swift it is a read-only variable.
So in your AppDelegate is like this:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// MARK: - UIApplicationDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIApplication.classInit
self.window = UIWindow(frame: UIScreen.main.bounds)
...
self.window?.makeKeyAndVisible()
return true
}
}
// MARK: - Fix Dynamic Type
extension UIApplication {
static let classInit: Void = {
method_exchangeImplementations(
class_getInstanceMethod(UIApplication.self, #selector(getter: fixedPreferredContentSizeCategory))!,
class_getInstanceMethod(UIApplication.self, #selector(getter: preferredContentSizeCategory))!
)
}()
#objc
var fixedPreferredContentSizeCategory: UIContentSizeCategory {
return .large
}
}

Okay, first let me say this: while I am happy that I was able to quickly find a way to accommodate the dynamic text provided by the iOS accessibility settings (which I will show the code for in a sec) I think it is still important to get an answer to the original question.
That said, here is what I did to the table view code to respect the larger type that some users need. It was a two step process. First, add:
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
to the viewDidLoad method. Then, in the cellForRowAtIndexPath method, add the following before you return the cell:
cell.textLabel!.numberOfLines = 0
Good luck folks, and please add an answer to the original question if you have one :)

What I would like to do is straight up disable the dynamic type within the app so it always displays the same sized type in the cells.
Dynamic Type only works for text with implemented text styles.
So, if you always want to disable Dynamic Type and display the same sized type in the cells, don't use text styles nor image size adjustment in them.
However, if you do want to use the text styles, never tick Automatically Adjusts Font for each text element in Interface Builder (equivalent to adjustsFontForContentSizeCategoryin code).

Hi All Frustrated Devs,
Here is perfect solution to disable Dynamic Type is :
Since iOS 15 you can set limits on the minimum and maximum sizes of dynamic type. It works for both UIKit and SwiftUI.
// UIKit
view.minimumContentSizeCategory = .medium
view.maximumContentSizeCategory = .accessibilityExtraLarge
// SwiftUI
ContentView()
.dynamicTypeSize(.medium ... .accessibility3)
Also if you want to directly disable throughout the whole Application, You should create a Base Class which should be parent of your all VCs. In BaseVC you can set up code.

Related

How to disable default keyboard navigation in Mac Catalyst app?

I noticed that I can step through rows in a UITableView in a Mac Catalyst app by pressing the up and down arrow keys on my Mac keyboard. However, this interferes with the existing functionality in one of my view controllers. Is there a way to disable this?
I can't find any reference to this functionality in the UITableView documentation. The Human Interface Guidelines for Mac Catalyst mentions "automatic support for fundamental Mac features, such as ... keyboard navigation," so I guess this is an intentional feature, but I can't find any further reference to it or documentation for it.
I haven't seen any other examples of "automatic" keyboard navigation in my app, but ideally Apple would publish a complete list so we could know how to work with, or if needed, disable, the built-in functionality.
Update as of 2021/11/06
It looks like Apple has been changing how the default focus system works and my previous solution is no longer working or required.
UIKeyCommand has a new wantsPriorityOverSystemBehavior: Bool property which needs to be set to true in order for our subclasses to receive certain types of commands, including the arrow key commands.
As of at least Xcode 13.1 and macOS 11.6, maybe eariler, we can now simply add the following to a UITableViewController subclass to replace the default focus behavior with custom keyboard navigation handing:
class TableViewController: UITableViewController {
override var keyCommands: [UIKeyCommand]? {
let upArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputUpArrow,
modifierFlags: [],
action: #selector(handleUpArrowKeyPress)
)
upArrowCommand.wantsPriorityOverSystemBehavior = true
let downArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputDownArrow,
modifierFlags: [],
action: #selector(handleDownArrowKeyPress)
)
downArrowCommand.wantsPriorityOverSystemBehavior = true
return [
upArrowCommand,
downArrowCommand
]
}
#objc
func handleUpArrowKeyPress () {
}
#objc
func handleDownArrowKeyPress () {
}
}
Previous answer (no longer working or required)
Catalyst automatically assigns UIKeyCommands for the up/down arrows to UITableView instances. This does not happen on iOS. You can see this in action by setting a break point in viewDidLoad() of a UITableViewController and inspecting tableView.keyCommands.
So I created a very simple UITableView subclass and disabled the default keyCommmands by returning nil:
class KeyCommandDisabledTableView: UITableView {
override var keyCommands: [UIKeyCommand]? {
return nil
}
}
I then updated my UITableViewController subclass to use the new KeyCommandDisabledTableView subclass:
class MyTableViewController: UITableViewController {
override func loadView() {
self.view = KeyCommandDisabledTableView(
frame: .zero,
style: .plain // or .grouped
)
}
}
Et voilà! The default arrow key handling is gone and my app's custom arrow key handling is now being called.
Here's another solution I received from Apple DTS. Just add this to the table view delegate:
func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool {
return false
}
This works in macOS 11.6 and 12.0. I don't have a 10.15 or 11.5 Mac to test with, so I'll keep my earlier resignFirstResponder solution, too.
I further noticed that the default arrow key navigation only begins after clicking a row in a table, so I guessed the table must be assuming the first responder role. I added this to my table's delegate class:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
#if TARGET_OS_MACCATALYST
[tableView performSelector:#selector(resignFirstResponder) withObject:nil afterDelay:0.1];
#endif
}
That fixed it! Now the default keyboard navigation turns off as soon as it turns on, and doesn't interfere with my app's custom keyboard navigation.
(It didn't work without the delay.)
iOS 14 / macOS 11 makes it much easier to disable this behavior thanks to UITableView and UICollectionView's selectionFollowsFocus property:
tableView.selectionFollowsFocus = false

iOS 13 disable Dark Mode changes [duplicate]

A large part of my app consists of web views to provide functionality not yet available through native implementations. The web team has no plans to implement a dark theme for the website. As such, my app will look a bit half/half with Dark Mode support on iOS 13.
Is it possible to opt out of Dark Mode support such that our app always shows light mode to match the website theme?
First, here is Apple's entry related to opting out of dark mode.
The content at this link is written for Xcode 11 & iOS 13:
Entire app via info.plist file (Xcode 12)
Use the following key in your info.plist file:
UIUserInterfaceStyle
And assign it a value of Light.
The XML for the UIUserInterfaceStyle assignment:
<key>UIUserInterfaceStyle</key>
<string>Light</string>
Apple documentation for UIUserInterfaceStyle
Entire app via info.plist in build settings (Xcode 13)
Entire app window via window property
You can set overrideUserInterfaceStyle against the app's window variable. This will apply to all views that appear within the window. This became available with iOS 13, so for apps that support previous versions, you must include an availability check.
Depending on how your project was created, this may be in the AppDelegate or SceneDelegate file.
if #available(iOS 13.0, *) {
window?.overrideUserInterfaceStyle = .light
}
Individual UIViewController or UIView
You can set overrideUserInterfaceStyle against the UIViewControllers or UIView's overrideUserInterfaceStyle variable. This became available with iOS 13, so for apps that support previous versions, you must include an availability check.
Swift
override func viewDidLoad() {
super.viewDidLoad()
// overrideUserInterfaceStyle is available with iOS 13
if #available(iOS 13.0, *) {
// Always adopt a light interface style.
overrideUserInterfaceStyle = .light
}
}
For those poor souls in Objective-C
if (#available(iOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
When set against the UIViewController, the view controller and its children adopt the defined mode.
When set against the UIView, the view and its children adopt the defined mode.
Apple documentation for overrideUserInterfaceStyle
Individual views via SwiftUI View
You can set preferredColorScheme to be either light or dark. The provided value will set the color scheme for the presentation.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Light Only")
.preferredColorScheme(.light)
}
}
Apple documentation for preferredColorScheme
Credit to #Aron Nelson, #Raimundas Sakalauskas, #NSLeader and #rmaddy for improving this answer with their feedback.
According to Apple's session on "Implementing Dark Mode on iOS" (https://developer.apple.com/videos/play/wwdc2019/214/ starting at 31:13) it is possible to set overrideUserInterfaceStyle to UIUserInterfaceStyleLight or UIUserInterfaceStyleDark on any view controller or view, which will the be used in the traitCollection for any subview or view controller.
As already mentioned by SeanR, you can set UIUserInterfaceStyle to Light or Dark in your app's plist file to change this for your whole app.
If you are not using Xcode 11 or later (i,e iOS 13 or later SDK), your app has not automatically opted to support dark mode. So, there's no need to opt out of dark mode.
If you are using Xcode 11 or later, the system has automatically enabled dark mode for your app. There are two approaches to disable dark mode depending on your preference. You can disable it entirely or disable it for any specific window, view, or view controller.
Disable Dark Mode Entirely for your App
You can disable dark mode by including the UIUserInterfaceStyle key with a value as Light in your app’s Info.plist file.
This ignores the user's preference and always applies a light appearance to your app.
Disable dark mode for Window, View, or View Controller
You can force your interface to always appear in a light or dark style by setting the overrideUserInterfaceStyle property of the appropriate window, view, or view controller.
View controllers:
override func viewDidLoad() {
super.viewDidLoad()
/* view controller’s views and child view controllers
always adopt a light interface style. */
overrideUserInterfaceStyle = .light
}
Views:
// The view and all of its subviews always adopt light style.
youView.overrideUserInterfaceStyle = .light
Window:
/* Everything in the window adopts the style,
including the root view controller and all presentation controllers that
display content in that window.*/
window.overrideUserInterfaceStyle = .light
Note: Apple strongly encourages to support dark mode in your app. So,
you can only disable dark mode temporarily.
Read more here: Choosing a Specific Interface Style for Your iOS App
Xcode 12 and iOS 14 update. I have try the previous options to opt-out dark mode and this sentence in the info.plist file is not working for me:
<key>UIUserInterfaceStyle</key>
<string>Light</string>
Now it is renamed to:
<key>Appearance</key>
<string>Light</string>
This setting will block all dark mode in the full app.
EDITED:
Fixed typo thank you to #sarah
********** Easiest way for Xcode 11 and above ***********
Add this to info.plist before </dict></plist>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
I think I've found the solution. I initially pieced it together from UIUserInterfaceStyle - Information Property List and UIUserInterfaceStyle - UIKit, but have now found it actually documented at Choosing a specific interface style for your iOS app.
In your info.plist, set UIUserInterfaceStyle (User Interface Style) to 1 (UIUserInterfaceStyle.light).
EDIT: As per dorbeetle's answer, a more appropriate setting for UIUserInterfaceStyle may be Light.
The answer above works if you want to opt out the whole app. If you are working on the lib that has UI, and you don't have luxury of editing .plist, you can do it via code too.
If you are compiling against iOS 13 SDK, you can simply use following code:
Swift:
if #available(iOS 13.0, *) {
self.overrideUserInterfaceStyle = .light
}
Obj-C:
if (#available(iOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
HOWEVER, if you want your code to compile against iOS 12 SDK too (which right now is still the latest stable SDK), you should resort to using selectors. Code with selectors:
Swift (XCode will show warnings for this code, but that's the only way to do it for now as property does not exist in SDK 12 therefore won't compile):
if #available(iOS 13.0, *) {
if self.responds(to: Selector("overrideUserInterfaceStyle")) {
self.setValue(UIUserInterfaceStyle.light.rawValue, forKey: "overrideUserInterfaceStyle")
}
}
Obj-C:
if (#available(iOS 13.0, *)) {
if ([self respondsToSelector:NSSelectorFromString(#"overrideUserInterfaceStyle")]) {
[self setValue:#(UIUserInterfaceStyleLight) forKey:#"overrideUserInterfaceStyle"];
}
}
For the entire App: (in the info.plist file):
<key>UIUserInterfaceStyle</key>
<string>Light</string>
Window (Usually the whole app):
window!.overrideUserInterfaceStyle = .light
You can get the window from SceneDelegate
UIViewController:
viewController.overrideUserInterfaceStyle = .light
You can set any viewController, even inside the viewController itself
UIView:
view.overrideUserInterfaceStyle = .light
You can set any view, even inside the view itself
You may need to use if #available(iOS 13.0, *) { ,,, } if you are supporting earlier iOS versions.
SwiftUI View:
.preferredColorScheme(.light) <- This Modifier
or
.environment(\.colorScheme, .light) <- This Modifier
You can turn Dark Mode off in entire application in Xcode 11:
Go Info.plist
Add bellow like
<key>UIUserInterfaceStyle</key>
<string>Light</string>
Info.plist will be look like below...
Swift 5
You can download a demo project with this link:- https://github.com/rashidlatif55/DarkLightMode
Two ways to switch dark to light mode:
1- info.plist
<key>UIUserInterfaceStyle</key>
<string>Light</string>
2- Programmatically or Runtime
#IBAction private func switchToDark(_ sender: UIButton){
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
}
Latest Update-
If you're using Xcode 10.x, then the default UIUserInterfaceStyle is light for iOS 13.x. When run on an iOS 13 device, it will work in Light Mode only.
No need to explicitly add the UIUserInterfaceStyle key in Info.plist file, adding it will give an error when you Validate your app, saying:
Invalid Info.plist Key. The key 'UIUserInterfaceStyle' in the Payload/AppName.appInfo.plist file is not valid.
Only add the UIUserInterfaceStyle key in Info.plist file when using Xcode 11.x.
iOS 14.3 and Xcode 12.3 Update
In info.plist file add Appearance as Light.
<key>Appearance</key>
<string>Light</string>
If you will add UIUserInterfaceStyle key to the plist file, possibly Apple will reject release build as mentioned here: https://stackoverflow.com/a/56546554/7524146
Anyway it's annoying to explicitly tell each ViewController self.overrideUserInterfaceStyle = .light. But you can use this peace of code once for your root window object:
if #available(iOS 13.0, *) {
if window.responds(to: Selector(("overrideUserInterfaceStyle"))) {
window.setValue(UIUserInterfaceStyle.light.rawValue, forKey: "overrideUserInterfaceStyle")
}
}
Just notice you can't do this inside application(application: didFinishLaunchingWithOptions:) because for this selector will not respond true at that early stage. But you can do it later on. It's super easy if you are using custom AppPresenter or AppRouter class in your app instead of starting UI in the AppDelegate automatically.
Apart from other responses, from my understanding of the following, you only need to prepare for Dark mode when compiling against iOS 13 SDK (using XCode 11).
The system assumes that apps linked against the iOS 13 or later SDK
support both light and dark appearances. In iOS, you specify the
specific appearance you want by assigning a specific interface style
to your window, view, or view controller. You can also disable support
for Dark Mode entirely using an Info.plist key.
Link
In Xcode 12, you can change add as "appearances". This will work!!
My app does not support dark mode as of now and uses a light app bar color. I was able to force the status bar content to dark text and icons by adding the following key to my Info.plist:
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDarkContent</string>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
Find the other possible values here: https://developer.apple.com/documentation/uikit/uistatusbarstyle
Flutter Users
Don't forget to set the app bar brightness attribute on your Flutter app bar like this:
AppBar(
backgroundColor: Colors.grey[100],
brightness: Brightness.light, // <---------
title: const Text('Hi there'),
),
Yes you can skip by adding the following code in viewDidLoad:
if #available(iOS 13.0, *) {
// Always adopt a light interface style.
overrideUserInterfaceStyle = .light
}
Objective-c version
if (#available(iOS 13.0, *)) {
_window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
Add this to info.plist
<key>UIUserInterfaceStyle</key>
<string>Light</string>
Yes.. you can add below setting in iOS project.
In info.plist file add UIUserInterfaceStyle to Light.
If your project is in IONIC.. You can add below setting in config file
<platform name="ios">
<edit-config file="*-Info.plist" mode="merge" target="UIUserInterfaceStyle">
<string>Light</string>
</edit-config>
</platform>
Using these settings, device dark mode will not affect your app.
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
} else {
// Fallback on earlier versions
}
I would use this solution since window property may be changed during the app life cycle. So assigning "overrideUserInterfaceStyle = .light" needs to be repeated. UIWindow.appearance() enables us to set default value that will be used for newly created UIWindow objects.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13.0, *) {
UIWindow.appearance().overrideUserInterfaceStyle = .light
}
return true
}
}
Here are a few tips and tricks which you can use in your app to support or bypass the dark mode.
First tip: To override the ViewController style
you can override the interface style of UIViewController by
1: overrideUserInterfaceStyle = .dark //For dark mode
2: overrideUserInterfaceStyle = .light //For light mode
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
}
}
Second tip: Adding a key in info.plist
Simply you can add a new key
UIUserInterfaceStyle
in your app info.plist and set its value to Light or Dark. this will override the app default style to the value you provide.
You don't have to add overrideUserInterfaceStyle = .light this line in every viewController, just one line in info.plist that’s it.
Just simply add following key in your info.plist file :
<key>UIUserInterfaceStyle</key>
<string>Light</string>
Just add these line in info.plist file:
<key>UIUserInterfaceStyle</key>
<string>light</string>
This will force app to run in light mode only.
add overrideUserInterfaceStyle = .light in ViewController.swift file or change Appearance to "light" in info.plist file
import UIKit
extension UIViewController {
override open func awakeFromNib() {
super.awakeFromNib()
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
}
}
}
You can do: add this new key UIUserInterfaceStyle to Info.plist and set its value to Light. and check alert controller appears with light mode.
UIUserInterfaceStyle
Light
If you are force light/dark mode in your whole application regardless of the user's settings by adding the key UIUserInterfaceStyle to your Info.plist file and setting its value to either Light or Dark.
This question has so many answers, rather using it in info.plist you can set it in AppDelegate like this:
#if compiler(>=5.1)
if #available(iOS 13.0, *) {
self.window?.overrideUserInterfaceStyle = .light
}
#endif
Test on Xcode 11.3, iOS 13.3
Actually I just wrote some code that will allow you to globally opt out of dark mode in code without having to putz with every single viw controller in your application. This can probably be refined to opt out on a class by class basis by managing a list of classes. For me, what I want is for my users to see if they like the dark mode interface for my app, and if they don't like it, they can turn it off. This will allow them to continue using dark mode for the rest of their applications.
User choice is good (Ahem, looking at you Apple, this is how you should have implemented it).
So how this works is that it's just a category of UIViewController. When it loads it replaces the native viewDidLoad method with one that will check a global flag to see if dark mode is disabled for everything or not.
Because it is triggered on UIViewController loading it should automatically start up and disable dark mode by default. If this is not what you want, then you need to get in there somewhere early and set the flag, or else just set the default flag.
I haven't yet written anything to respond to the user turning the flag on or off. So this is basically example code. If we want the user to interact with this, all the view controllers will need to reload. I don't know how to do that offhand but probably sending some notification is going to do the trick. So right now, this global on/off for dark mode is only going to work at startup or restart of the app.
Now, it's not just enough to try to turn off dark mode in every single MFING viewController in your huge app. If you're using color assets you are completely boned. We for 10+ years have understood immutable objects to be immutable. Colors you get from the color asset catalog say they are UIColor but they are dynamic (mutable) colors and will change underneath you as the system changes from dark to light mode. That is supposed to be a feature. But of course there is no master toggle to ask these things to stop making this change (as far as I know right now, maybe someone can improve this).
So the solution is in two parts:
a public category on UIViewController that gives some utility and convenience methods... for instance I don't think apple has thought about the fact that some of us mix in web code into our apps. As such we have stylesheets that need to be toggled based on dark or light mode. Thus, you either need to build some kind of a dynamic stylesheet object (which would be good) or just ask what the current state is (bad but easy).
this category when it loads will replace the viewDidLoad method of the UIViewController class and intercept calls. I don't know if that breaks app store rules. If it does, there are other ways around that probably but you can consider it a proof of concept. You can for instance make one subclass of all the main view controller types and make all of your own view controllers inherit from those, and then you can use the DarkMode category idea and call into it to force opt out all of your view controllers. It is uglier but it is not going to break any rules. I prefer using the runtime because that's what the runtime was made to do. So in my version you just add the category, you set a global variable on the category for whether or not you want it to block dark mode, and it will do it.
You are not out of the woods yet, as mentioned, the other problem is UIColor basically doing whatever the hell it wants. So even if your view controllers are blocking dark mode UIColor doesn't know where or how you're using it so can't adapt. As a result you can fetch it correctly but then it's going to revert on you at some point in the future. Maybe soon maybe later. So the way around that is by allocating it twice using a CGColor and turning it into a static color. This means if your user goes back and re-enables dark mode on your settings page (the idea here is to make this work so that the user has control over your app over and above the rest of the system), all of those static colors need replacing. So far this is left for someone else to solve. The easy ass way to do it is to make a default that you're opting out of dark mode, divide by zero to crash the app since you can't exit it and tell the user to just restart it. That probably violates app store guidelines as well but it's an idea.
The UIColor category doesn't need to be exposed, it just works calling colorNamed: ... if you didn't tell the DarkMode ViewController class to block dark mode, it will work perfectly nicely as expected. Trying to make something elegant instead of the standard apple sphaghetti code which is going to mean you're going to have to modify most of your app if you want to programatically opt out of dark mode or toggle it. Now I don't know if there is a better way of programatically altering the Info.plist to turn off dark mode as needed. As far as my understanding goes that's a compile time feature and after that you're boned.
So here is the code you need. Should be drop in and just use the one method to set the UI Style or set the default in the code. You are free to use, modify, do whatever you want with this for any purpose and no warranty is given and I don't know if it will pass the app store. Improvements very welcome.
Fair warning I don't use ARC or any other handholding methods.
////// H file
#import <UIKit/UIKit.h>
#interface UIViewController(DarkMode)
// if you want to globally opt out of dark mode you call these before any view controllers load
// at the moment they will only take effect for future loaded view controllers, rather than currently
// loaded view controllers
// we are doing it like this so you don't have to fill your code with #availables() when you include this
typedef enum {
QOverrideUserInterfaceStyleUnspecified,
QOverrideUserInterfaceStyleLight,
QOverrideUserInterfaceStyleDark,
} QOverrideUserInterfaceStyle;
// the opposite condition is light interface mode
+ (void)setOverrideUserInterfaceMode:(QOverrideUserInterfaceStyle)override;
+ (QOverrideUserInterfaceStyle)overrideUserInterfaceMode;
// utility methods
// this will tell you if any particular view controller is operating in dark mode
- (BOOL)isUsingDarkInterfaceStyle;
// this will tell you if any particular view controller is operating in light mode mode
- (BOOL)isUsingLightInterfaceStyle;
// this is called automatically during all view controller loads to enforce a single style
- (void)tryToOverrideUserInterfaceStyle;
#end
////// M file
//
// QDarkMode.m
#import "UIViewController+DarkMode.h"
#import "q-runtime.h"
#implementation UIViewController(DarkMode)
typedef void (*void_method_imp_t) (id self, SEL cmd);
static void_method_imp_t _nativeViewDidLoad = NULL;
// we can't #available here because we're not in a method context
static long _override = -1;
+ (void)load;
{
#define DEFAULT_UI_STYLE UIUserInterfaceStyleLight
// we won't mess around with anything that is not iOS 13 dark mode capable
if (#available(iOS 13,*)) {
// default setting is to override into light style
_override = DEFAULT_UI_STYLE;
/*
This doesn't work...
NSUserDefaults *d = NSUserDefaults.standardUserDefaults;
[d setObject:#"Light" forKey:#"UIUserInterfaceStyle"];
id uiStyle = [d objectForKey:#"UIUserInterfaceStyle"];
NSLog(#"%#",uiStyle);
*/
if (!_nativeViewDidLoad) {
Class targetClass = UIViewController.class;
SEL targetSelector = #selector(viewDidLoad);
SEL replacementSelector = #selector(_overrideModeViewDidLoad);
_nativeViewDidLoad = (void_method_imp_t)QMethodImplementationForSEL(targetClass,targetSelector);
QInstanceMethodOverrideFromClass(targetClass, targetSelector, targetClass, replacementSelector);
}
}
}
// we do it like this because it's not going to be set often, and it will be tested often
// so we can cache the value that we want to hand to the OS
+ (void)setOverrideUserInterfaceMode:(QOverrideUserInterfaceStyle)style;
{
if (#available(iOS 13,*)){
switch(style) {
case QOverrideUserInterfaceStyleLight: {
_override = UIUserInterfaceStyleLight;
} break;
case QOverrideUserInterfaceStyleDark: {
_override = UIUserInterfaceStyleDark;
} break;
default:
/* FALLTHROUGH - more modes can go here*/
case QOverrideUserInterfaceStyleUnspecified: {
_override = UIUserInterfaceStyleUnspecified;
} break;
}
}
}
+ (QOverrideUserInterfaceStyle)overrideUserInterfaceMode;
{
if (#available(iOS 13,*)){
switch(_override) {
case UIUserInterfaceStyleLight: {
return QOverrideUserInterfaceStyleLight;
} break;
case UIUserInterfaceStyleDark: {
return QOverrideUserInterfaceStyleDark;
} break;
default:
/* FALLTHROUGH */
case UIUserInterfaceStyleUnspecified: {
return QOverrideUserInterfaceStyleUnspecified;
} break;
}
} else {
// we can't override anything below iOS 12
return QOverrideUserInterfaceStyleUnspecified;
}
}
- (BOOL)isUsingDarkInterfaceStyle;
{
if (#available(iOS 13,*)) {
if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark){
return YES;
}
}
return NO;
}
- (BOOL)isUsingLightInterfaceStyle;
{
if (#available(iOS 13,*)) {
if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight){
return YES;
}
// if it's unspecified we should probably assume light mode, esp. iOS 12
}
return YES;
}
- (void)tryToOverrideUserInterfaceStyle;
{
// we have to check again or the compile will bitch
if (#available(iOS 13,*)) {
[self setOverrideUserInterfaceStyle:(UIUserInterfaceStyle)_override];
}
}
// this method will be called via the viewDidLoad chain as we will patch it into the
// UIViewController class
- (void)_overrideModeViewDidLoad;
{
if (_nativeViewDidLoad) {
_nativeViewDidLoad(self,#selector(viewDidLoad));
}
[self tryToOverrideUserInterfaceStyle];
}
#end
// keep this in the same file, hidden away as it needs to switch on the global ... yeah global variables, I know, but viewDidLoad and colorNamed: are going to get called a ton and already it's adding some inefficiency to an already inefficient system ... you can change if you want to make it a class variable.
// this is necessary because UIColor will also check the current trait collection when using asset catalogs
// so we need to repair colorNamed: and possibly other methods
#interface UIColor(DarkMode)
#end
#implementation UIColor (DarkMode)
typedef UIColor *(*color_method_imp_t) (id self, SEL cmd, NSString *name);
static color_method_imp_t _nativeColorNamed = NULL;
+ (void)load;
{
// we won't mess around with anything that is not iOS 13 dark mode capable
if (#available(iOS 13,*)) {
// default setting is to override into light style
if (!_nativeColorNamed) {
// we need to call it once to force the color assets to load
Class targetClass = UIColor.class;
SEL targetSelector = #selector(colorNamed:);
SEL replacementSelector = #selector(_overrideColorNamed:);
_nativeColorNamed = (color_method_imp_t)QClassMethodImplementationForSEL(targetClass,targetSelector);
QClassMethodOverrideFromClass(targetClass, targetSelector, targetClass, replacementSelector);
}
}
}
// basically the colors you get
// out of colorNamed: are dynamic colors... as the system traits change underneath you, the UIColor object you
// have will also change since we can't force override the system traits all we can do is force the UIColor
// that's requested to be allocated out of the trait collection, and then stripped of the dynamic info
// unfortunately that means that all colors throughout the app will be static and that is either a bug or
// a good thing since they won't respond to the system going in and out of dark mode
+ (UIColor *)_overrideColorNamed:(NSString *)string;
{
UIColor *value = nil;
if (#available(iOS 13,*)) {
value = _nativeColorNamed(self,#selector(colorNamed:),string);
if (_override != UIUserInterfaceStyleUnspecified) {
// the value we have is a dynamic color... we need to resolve against a chosen trait collection
UITraitCollection *tc = [UITraitCollection traitCollectionWithUserInterfaceStyle:_override];
value = [value resolvedColorWithTraitCollection:tc];
}
} else {
// this is unreachable code since the method won't get patched in below iOS 13, so this
// is left blank on purpose
}
return value;
}
#end
There is a set of utility functions that this uses for doing method swapping. Separate file. This is standard stuff though and you can find similar code anywhere.
// q-runtime.h
#import <Foundation/Foundation.h>
#import <objc/message.h>
#import <stdatomic.h>
// returns the method implementation for the selector
extern IMP
QMethodImplementationForSEL(Class aClass, SEL aSelector);
// as above but gets class method
extern IMP
QClassMethodImplementationForSEL(Class aClass, SEL aSelector);
extern BOOL
QClassMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector);
extern BOOL
QInstanceMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector);
// q-runtime.m
static BOOL
_QMethodOverride(Class targetClass, SEL targetSelector, Method original, Method replacement)
{
BOOL flag = NO;
IMP imp = method_getImplementation(replacement);
// we need something to work with
if (replacement) {
// if something was sitting on the SEL already
if (original) {
flag = method_setImplementation(original, imp) ? YES : NO;
// if we're swapping, use this
//method_exchangeImplementations(om, rm);
} else {
// not sure this works with class methods...
// if it's not there we want to add it
flag = YES;
const char *types = method_getTypeEncoding(replacement);
class_addMethod(targetClass,targetSelector,imp,types);
XLog_FB(red,black,#"Not sure this works...");
}
}
return flag;
}
BOOL
QInstanceMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector)
{
BOOL flag = NO;
if (targetClass && replacementClass) {
Method om = class_getInstanceMethod(targetClass,targetSelector);
Method rm = class_getInstanceMethod(replacementClass,replacementSelector);
flag = _QMethodOverride(targetClass,targetSelector,om,rm);
}
return flag;
}
BOOL
QClassMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector)
{
BOOL flag = NO;
if (targetClass && replacementClass) {
Method om = class_getClassMethod(targetClass,targetSelector);
Method rm = class_getClassMethod(replacementClass,replacementSelector);
flag = _QMethodOverride(targetClass,targetSelector,om,rm);
}
return flag;
}
IMP
QMethodImplementationForSEL(Class aClass, SEL aSelector)
{
Method method = class_getInstanceMethod(aClass,aSelector);
if (method) {
return method_getImplementation(method);
} else {
return NULL;
}
}
IMP
QClassMethodImplementationForSEL(Class aClass, SEL aSelector)
{
Method method = class_getClassMethod(aClass,aSelector);
if (method) {
return method_getImplementation(method);
} else {
return NULL;
}
}
I'm copying and pasting this out of a couple of files since the q-runtime.h is my reusable library and this is just a part of it. If something doesn't compile let me know.

UITableView is resetting its background color before view appears

I'm using probably a little bit exotic way of initialization of my UI components. I create them programmatically and among them is a UITableView instance, I set its background color immediately upon initialization, like this:
class MyViewController: UIViewController {
...
let tableView = UITableView().tap {
$0.backgroundColor = .black
$0.separatorStyle = .none
}
...
}
where tap is extension function:
func tap(_ block: (Self) -> Void) -> Self {
block(self)
return self
}
This worked very well in my previous project which was created in Xcode 8 and then migrated to Xcode 9 without breaking anything. But now I've created brand new project in Xcode 9 and copy-pasted above-mentioned extension to it, but seems like something went wrong. When my view controller appears on screen table has white background and default separator insets.
This seems to affect only some of the properties because others are working as they should have (e.g. $0.register(nib: UINib?, forCellReuseIdentifier: String) registers required cell class and $0.showsVerticalScrollIndicator = false hides scroll indicator).
Perhaps some of you, guys, could give me an idea what's the heart of the matter.
Here's full code, to reproduce the issue simply create a new project and replace ViewController.swift's content. As you can see, table has correct rowHeight (160) but resets its background color.
As for "before view appears" statement: I've printed table's background color in viewDidLoad, viewWillAppear and viewDidAppear like this:
print(#function, table.backgroundColor.debugDescription)
– it changes its color only in the last debug print:
viewDidLoad() Optional(UIExtendedGrayColorSpace 0 1)
viewWillAppear Optional(UIExtendedGrayColorSpace 0 1)
viewDidAppear Optional(UIExtendedSRGBColorSpace 1 1 1 1)
I ended up moving the initialization to lazy var's function – turns out initializing UITableView during the initialization of it's view controller has some side effects.

Passing Data from SettingsView Controller to Mainstoryboard - Swift

I am a newbie to iOS app development trying to build a tip calculator. The basic functioning of the app is completed. My Mainstoryboard has a segment control which shows three % values such as 10, 20, 30. There is a Settings button which on click takes me to a new page and shows the similar % values in a segment control.
What I want to do is that, when a number is clicked (a segment) it should be saved as the default tip% value. How should I pass this value to the Mainstoryboard function where I have written a function to calculate the tip amount?
I suppose you are using a segue for moving to the other view, so use prepareForSegue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "tipSegue"
{
if let destinationVC = segue.destinationViewController as? OtherViewController {
destinationVC.tipPercent = someValue
}
}
}
You can write a protocol for setting the default percentage, and have your MainViewController conform to that protocol. When a user taps a button in your SettingsViewController you can call a function (defined within the protocol) to let the "delegate" (MainViewController) know what has happened, and set your default variable accordingly. If this is a permanent setting it might be better to use UserDefaults, as this is exactly what that was designed for. Then, upon loading MainViewController you can access that UserDefaults variable, if it exists.
A little protocol help:
Your protocol could be defined very simply - something like this (and this has to be declared outside of your viewControllers - I often do it above the class declaration of a related viewController):
protocol SettingsDelegate {
func didUpdateDefaultTipPercent(to percent: Float)
}
That's it for the protocol declaration, unless you anticipate needing other functions. Note that you don't actually define the functionality of that function - each viewController that conforms to the protocol may have a different definition of that.
Then, in your settingsViewController you might have an optional variable for protocol conformer like this:
weak var settingsDelegate: SettingsDelegate?
and after the user chooses a default percentage you can safely check to see if the delegate exists and pass that new number to the delegate like so:
if let delegate = self.settingsDelegate {
delegate.didUpdateDefaultTipPercent(to: 15.0) //this "15.0" will come from your segmented control action or whatever
}
In your MainViewController, in your prepareFor(segue... you will need to check to see if you are going to settings, and set Main as the delegate:
...
if let settings = segue.destination as? SettingsViewController {
settings.settingsDelegate = self
}
And, finally, you'll need to make your MainViewController conform to the protocol. I usually do this as an extension just to make it easier to find, and to keep it separated from other things:
extension MainViewController: SettingsDelegate {
func didUpdateDefaultTipPercent(to percent: Float) {
self.defaultPercentage = percent
}
}

UIView doesn't change at runtime

I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.

Resources