adding user defaults to dark mode - ios

This is a continuation of an earlier post. What I was wondering was how to add the user defaults for the dark mode throughout the app. Please do not pay attention for the code that says UserDefaults in my last post, I was following a tutorial and just kind of copied what he did, not knowing anything at all about User Defaults. The whole dark mode works beautifully throughout the app. I just need to know how to do all the user defaults. If you have any questions feel free to ask.
The code below is what the custom cell looks like below that is in a settings view controller, to change the app to a Dark Mode. Everything works great and as it should. I just need to put in the user defaults into the actions.
import UIKit
class DarkModeTableViewCell: UITableViewCell {
var DarkisOn = Bool()
let userDefaults = UserDefaults.standard
#IBOutlet var darkModeSwitchOutlet: UISwitch!
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(darkModeEnabled(_:)), name: .darkModeEnabled, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(darkModeDisabled(_:)), name: .darkModeDisabled, object: nil)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func darkModeSwitched(_ sender: Any) {
if darkModeSwitchOutlet.isOn == true {
//enable dark mode
DarkisOn = true
//add a userDefault here so that the app will stay in dark mode
NotificationCenter.default.post(name: .darkModeEnabled, object: nil)
} else {
//enable light mode
DarkisOn = false
//add a userDefault here so that the app will stay in light mode
NotificationCenter.default.post(name: .darkModeDisabled, object: nil)
}
}
#objc private func darkModeEnabled(_ notification: Notification) {
DarkModeTableViewCellChange.instance.set(for: self)
textLabel?.textColor = UIColor.white
}
#objc private func darkModeDisabled(_ notification: Notification) {
LightModeTableViewCellChange.instance.set(for: self)
textLabel?.textColor = UIColor.black
}
}
EDIT: What I am looking for is how to add the user defaults to the dark mode. So once the dark mode is turned on, then when you close the app, it would stay on, etc.

Everything you do with NSUserDefaults is to store settings and retrieve them. You would store what theme your user is using in them.
So do something like this when changing your themes (in your previous question you were already doing something like this):
let defaults = UserDefaults.standard
// Do something like this when using changing your theme to dark mode.
defaults.set(true, "darkModeEnabled")
// Do something like this when changing your theme to your standard one
defaults.set(false, "darkModeEnabled")
In the viewWillAppear of your themable view controllers, you just check the value of the key you specified in UserDefaults.
/// Check if the user is using dark mode in viewDidLoad.
override func viewWillAppear() {
super.viewDidLoad()
let darkModeEnabled = defaults.bool(forKey: "darkModeEnabled")
if darkModeEnabled {
// Apply your dark theme
} else {
// Apply your normal theme.
}
}
This way your app your view will controllers will have the right theme upon loading, and the user will see the right one when loading the app.
Recommended reading: UserDefaults
As an aside note, the tutorial series you are following on YouTube is clearly not good enough for beginners, as it can be evidenced by the fact it mentions UserDefaults and even uses them but apparently never tells you how to use them. You should just get a good book on iOS development instead.

Related

app doesn't update and traitCollectionDidChange doesn't fire when user changes appearance in settings

In my app I allow the user to override the appearance that is set on the device. Here's the class that handles updating the UI if they want to override the system-wide appearance:
class UserInterfaceStyleController {
init() {
self.handleUserInterfaceStyleChange()
NotificationCenter.default.addObserver(self, selector: #selector(self.handleUserInterfaceStyleChange), name: Notification.Name(OMGNotification.changeBizzaroMode.rawValue), object: nil)
}
#objc func handleUserInterfaceStyleChange() {
if #available(iOS 13.0, *) {
let windows = UIApplication.shared.windows
for window in windows {
if UIScreen.main.traitCollection.userInterfaceStyle == .light {
if UserDefaults.standard.bizzaroMode {
window.overrideUserInterfaceStyle = .dark
} else {
window.overrideUserInterfaceStyle = .light
}
}
if UIScreen.main.traitCollection.userInterfaceStyle == .dark {
if UserDefaults.standard.bizzaroMode {
window.overrideUserInterfaceStyle = .light
} else {
window.overrideUserInterfaceStyle = .dark
}
}
window.subviews.forEach({ view in
view.layoutIfNeeded()
})
}
}
}
This works - the user flips the switch and the app changes the userInterfaceStyle. The problem is the app doesn't change appearance automatically when the user changes the system-wide appearance (set light or dark mode in settings), ONLY when they set it manually.
So I'm assuming I need to do some work when the user changes it system-wide, and traitCollectionDidChange seems to be what I need to use. Problem is it doesn't fire when the user changes the appearance in settings, ONLY when it's manually set in my app.
I've got this code in a viewController to test that:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
// Lots of stuff
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
print("We have a style mode change")
}
// Lots more stuff
}
When I manually override, it prints, "We have a style mode change". When I go into settings and switch the system-wide appearance traitCollectionDidChange doesn't fire.
I swear I had this working fine at one point, but I've been trying to fix some weird issues for a while now when I use overrideUserInterfaceStyle and have burned through a lot of code changes.
I think I'm missing something obvious. Before I started allowing the user to override, the app switched automatically in the background when the appearance was changed system-wide with no code at all. Now it doesn't and traitCollectionDidChange doesn't fire. What am I missing or doing wrong? Happy to provide more code if it could help.
Thanks!
The problem is that you override the user interface style, which causes any system changes not to become propagated to your window. You need to set overrideUserInterfaceStyle to .unspecified, otherwise the system won't call the traitCollectionDidChange methods in your view stack on change.
In our apps, we offer the user three options: automatic, light and dark, where each option just sets overrideUserInterfaceStyle to the appropriate option.
By the way, you don't need to call layoutIfNeeded() on all your subviews—setting overrideUserInterfaceStyle triggers a redraw automatically.

How do I set the color chosen by user as a background color for the entire IOS App?

I want the user to choose the preferred color by clicking the wanted button, and based on the choice it will be the background color of all pages in the App.
The code given is what I have used to change the background color through code, as shown in the image attached Screenshot of page sample . However, it changes temporary only and does not get saved when I move to another page and get back.
class ColorViewController: UIViewController {
#IBOutlet weak var redBtn: UIButton!
#IBOutlet weak var blueBtn: UIButton!
#IBAction func changeToRed(_ sender: UIButton) {
self.view.backgroundColor = UIColor.green
}
#IBAction func changeToBlue(_ sender: UIButton) {
self.view.backgroundColor = UIColor.cyan
}
If you also know how to save this color for a registered user (So every time the user signs in, the chosen background stays as is instead of reseting to default) it would be great. Please advise if you know the solution, I am new on Swift.
For saving user's color choice, you can use UserDefaults, that will allow you to store small sized information (like color preference) inside memory. So you can use something like this:
UserDefaults.standard.set(UIColor.red , forKey: "themePreferenceKey")
That will store Red choice in memory that'll be reachable with key themePreferenceKey later on.
For changing view/button background colors according to user's saved choice, you must fetch the value of themePreferenceKey whenever a view is Appeared, and change the colors accordingly. This can be implemented inside viewDidAppear method. Such as:
override func viewDidAppear(_ animated: Bool) {
if let preferredColor = UserDefaults.standard.object(forKey: "themePreferenceKey") as? UIColor {
self.view.backgroundColor = preferredColor
}
}

how to prevent UITextfield from changing alignment when input language is RTL/Arabic?

The app language is English, the illustration on the left shows UITextfields content aligned to the left.. which is normal, but when the user selects an RTL/Arabic input language, the fields alignment are flipped automatically, how to force the alignment to be left disregarding the input language direction?
EDIT :
I tried this, and it's not solving the problem
let _beginningOfDocument = fieldPassword!.beginningOfDocument
let _endOfDocument = fieldPassword!.endOfDocument
fieldPassword!.setBaseWritingDirection(.leftToRight, for: fieldPassword!.textRange(from: _beginningOfDocument , to: _endOfDocument )! )
To be honest I tried all of them and doesn't work for me because it's related to the view hierarchy . For more information, you can read this note
Note
iOS applies appearance changes when a view enters a window, it doesn’t change the appearance of a view that’s already in a window. To change the appearance of a view that’s currently in a window, remove the view from the view hierarchy and then put it back.
So try this one, it worked for me:
extension UITextField {
open override func awakeFromNib() {
super.awakeFromNib()
if Utilities.getCurrentLanguage() == "ar" {
if textAlignment == .natural {
self.textAlignment = .right
}
}
}
}
By the way, "Utilities.getCurrentLanguage()" is a function to get the current language.
It came out that some library I was using caused this effect, it's MOLH library, it uses method swizzling, this is why this was difficult to debug...
I will be making a pull request to it soon to make this effect optional...
func listenToKeyboard() {
NotificationCenter.default.removeObserver(self, name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
}
#objc func inputModeDidChange(_ notification: Notification) {
if let language = self.textInputMode?.primaryLanguage, MOLHLanguage.isRTLLanguage(language: language) {
self.textAlignment = .right
} else {
self.textAlignment = .left
}
}
Thanks.
In Swift 4
textField.semanticContentAttribute = UISemanticContentAttribute.forceLeftToRight
In your app delegate, add this:
if #available(iOS 9.0, *) {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}

Move keyboard above TabViewController TabBar

Is it possible to move the keyboard up so it doesn't cover the UITabViewController's TabBar?
Update after being given more context in comments
If your main concern is letting the user dismiss the keyboard, there are some well known patterns that are commonly applied on the platform:
Assumption regarding UI (derived from your comment):
- UITableView as main content
To make the keyboard dismissible, you can utilise a property on UIScrollView called .keyboardDismissMode. (UITableView is derived from UIScrollView, so it inherits the property.)
The default value for this property is .none. Change that to either .onDrag or .interactive. Consult the documentation for differences between the latter two options.
Behind the scenes, UIKit sets up a connection between the UIScrollView instance and any incoming keyboard. This allows the user to "swipe away" the keyboard by interacting with the scroll view.
Note that in order for this feature to work, your UIScrollView needs to be scrollable. To understand what 'scrollable' means in this context, please see this gist.
If your tableView has very few or no rows, it is likely not natively scrollable. To account for that, set tableView.alwaysBounceVertical = true. This will make sure your users can dismiss the keyboard regardless of the number of rows in the table.
Most of the popular apps handling keyboard dismissal also make it possible to dismiss the keyboard simply by tapping the content partially overlapped by it (in your case, the tableView). To enable this, you would simply have to install a UITapGestureRecognizer on your view and dismiss the keyboard in its action method:
class MyViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapRecognizer)
}
}
//MARK: - Tap handling
fileprivate extension MyViewController {
#objc func handleTap() {
if searchBar.isFirstResponder {
searchBar.resignFirstResponder()
}
// Alternative
// view.endEditing(true)
}
}
// -
Old answer
Yes, you can actually do this without using private API.
Disclaimer
You should really think about whether you actually want to do this. Opening the keyboard in virtually every use case should create a new "context" of editing which modally "blocks" other contexts (such as the navigation context provided by UITabBarController and its UITabBar). I guess one could make the point that users are able to leave an editing context by interacting with a potentially present UINavigationBar which is usually not blocked by keyboards. However, this is a known interaction throughout the system. Not blocking a UITabBar or UIToolbar while showing the keyboard on the other hand, is not. That being said, use the code below to move the keyboard up, but critically review the UX you are creating. I'm not to say it does never make sense to move the keyboard up, but you should really know what you're doing here. To be honest, it also looks kind of iffy, having the keyboard float above the tab bar.
Code
extension Sequence {
func last(where predicate: (Element) throws -> Bool) rethrows -> Element? {
return try reversed().first(where: predicate)
}
}
// Using `UIViewController` as an example. You could and actually should factor this logic out.
class MyViewController: UIViewController {
deinit {
NotificationCenter.default.removeObserver(self)
}
func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: .UIKeyboardWillHide, object: nil)
}
}
//MARK: - Keyboard handling
extension MyViewController {
private var keyboardOffset: CGFloat {
// Using a fixed value of `49` here, since that's what `UITabBar`s height usually is.
// You should probably use something like `-tabBarController?.tabBar.frame.height`.
return -49
}
private var keyboardWindowPredicate: (UIWindow) -> Bool {
return { $0.windowLevel > UIWindowLevelNormal }
}
private var keyboardWindow: UIWindow? {
return UIApplication.shared.windows.last(where: keyboardWindowPredicate)
}
#objc fileprivate func keyboardWillShow(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = keyboardOffset
}
}
#objc fileprivate func keyboardWillHide(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = 0
}
}
}
// -
Caution
Note that if you are using the .UIKeyboardWillShow and .UIKeyboardWillHide notifications to account for the keyboard in your view (setting UIScrollView insets, for example), you would have to also account for any additional offset by which you move keyboard window.
This works and is tested with iOS 11. However, there is no guarantee that the UIKit team won't change the order of windows or something else that breaks this in future releases. Again, you are not using any private API, so AppStore review should not be in danger, but you are doing something that you're not really supposed to do with the framework, and that can always come around and bite you later on.

iOS - keep layout in landscape but change controls

I'm done with the auto-layout stuff in my iOS universal App, and it's working perfectly in portrait. However, I want the user to be able to rotate the device and play the game in landscape mode. The problem I'm facing is that I don't want the layout to change at all, and only change the controls of the game (sliding up the screen should make the player go up in both orientations).
Thing is, I don't know how to prevent orientation from changing the layout and at the same time be able to change behaviour based on the orientation. Do you guys have any idea how I could manage that?
Did found a way to do, for future reference, when an orientation is disabled, we still can access device orientation (and not interface orientation), and register a notification to act upon change.
class ViewController: UIViewController {
var currentOrientation = 0
override func viewDidLoad() {
super.viewDidLoad()
// Register for notification about device orientation change
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate(notification:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
// Remove observer on window disappears
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
if UIDevice.current.isGeneratingDeviceOrientationNotifications {
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
}
// That part gets fired on orientation change, and I ignore states 0 - 5 - 6, respectively Unknown, flat up facing and down facing.
func deviceDidRotate(notification: NSNotification) {
if (UIDevice.current.orientation.rawValue < 5 && UIDevice.current.orientation.rawValue > 0) {
self.currentOrientation = UIDevice.current.orientation.rawValue
}
}
}

Resources