why the setter of var for KVO crashed (swift 2.2)? - ios

I'm using KVO for manual notifications, but why the code crashed for the reason:
Thread 1:EXC_BAD_ACCESS (code=2, address=0x7fff577bcfa8)" when click
run?
Please see below the codes:
ChildrenViewController.swift (class to be observed)
import UIKit
class ChildrenViewController: UIViewController {
dynamic var name: String? {
get {
return ""
}
set {
willChangeValueForKey("name")
guard let value = newValue else {return}
self.setValue(value, forKey: "name") //crashed here!SAID "Thread 1:EXC_BAD_ACCESS (code=2, address=0x7fff577bcfa8)"
didChangeValueForKey("name")
}
}
dynamic var age = 0
var child: ChildrenViewController?
override class func automaticallyNotifiesObserversForKey(key: String) -> Bool {
if key == "name" {
return false
}
return super.automaticallyNotifiesObserversForKey(key)
}
}
ViewController.swift (the observer)
import UIKit
private var child1Context = 1
class ViewController: UIViewController {
var child1 = ChildrenViewController()
override func viewDidLoad() {
super.viewDidLoad()
self.child1.setValue("George", forKey: "name")
self.child1.setValue(15, forKey: "age")
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
child1.addObserver(self, forKeyPath: "name", options: [.New,.Old], context: &child1Context)
child1.addObserver(self, forKeyPath: "age", options: [.New, .Old], context: &child1Context)
self.child1.name = "Michael" //set the name String
self.child1.setValue(20, forKey: "age")
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.child1.removeObserver(self, forKeyPath: "name")
self.child1.removeObserver(self, forKeyPath: "age")
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &child1Context {
if keyPath == "name" {
print("The name of FIRST has been changed, \(change)")
}
if keyPath == "age" {
print("The age of FIRST has been changed, \(change)")
}
}
}
}

You are setting value of name in it's own setter by this line:
self.setValue(value, forKey: "name")
Why can't do this:
private var _name: String?//create private variable to hold value
dynamic var name: String? {
get {
return _name
}
set {
willChangeValueForKey("name")
guard let value = newValue else {return}
_name = value
didChangeValueForKey("name")
}
}

You added addObserver in viewWillAppear method, so it means you added it every time when your screen is show. For this case you need call removeObserver in viewWillDisappear method
override func viewWillDisappear(animated: Bool) {
self.child1.removeObserver(self, forKeyPath: "name")
self.child1.removeObserver(self, forKeyPath: "age")
super.viewWillDisappear(animated)
}

Related

How to change language (localisation) within the app in swift 5?

I am trying to localise iOS app which is developed in Swift 5. I have done with all localisation things in code as well as in storyboard. But I am not sure how to change language within the app when i click on Language Button.
Is this possible to change app language within app? if yes How?
Please suggest best possible way to do same
I just did a similar implementation. Glad you asked and I saw this. Here is my implementation. You can modify.
enum Language: String, CaseIterable {
case english, german
var code: String {
switch self {
case .english: return "en"
case .german: return "de"
}
}
static var selected: Language {
set {
UserDefaults.standard.set([newValue.code], forKey: "AppleLanguages")
UserDefaults.standard.set(newValue.rawValue, forKey: "language")
}
get {
return Language(rawValue: UserDefaults.standard.string(forKey: "language") ?? "") ?? .english
}
}
static func switchLanguageBetweenEnglishAndGerman() {
selected = selected == .english ? .german : .english
}
}
Now you just need to call Language.selected == .german and reload the views.
To change localization throughout the app. For that, You need to follow the below step.
Create a Parent class of every UIViewController and define setupLocasitation method for further usage.
ParentViewController.swift
class ParentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func setupLocasitation(){
}
}
All other class of UIViewController should be a subclass of ParentViewController and override setupLocasitation method
ViewController1.swift
class ViewController1: ParentViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupLocasitation()
}
override func setupLocasitation() {
super.setupLocasitation()
print("Your localisation specifi code here...")
}
}
ViewController2.swift
class ViewController2: ParentViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupLocasitation()
}
override func setupLocasitation() {
super.setupLocasitation()
print("Your localisation specifi code here...")
}
}
ChangeLanguageVC.swift
You need to grab all instances of ParentViewController and force-fully call the setupLocasitation method.
class ChangeLanguageVC: ParentViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupLocasitation()
}
#IBAction func btnChangeLanguageTap(){
//Code for your language changes here...
let viewControllers = self.navigationController?.viewControllers ?? []
for vc in viewControllers{
if let parent = vc as? ParentViewController{
parent.setupLocasitation()
}
}
}
}
//
// LanguageExtensions.swift
// Flourish
//
// Created by Janko on 11/11/2020.
//
import Foundation
import UIKit
let languageKey = "languageKey"
var language : Int {
switch UserDefaults.standard.string(forKey: languageKey) {
case "en":
return 0
case "dutch":
return 1
default:
return 0
}
}
extension String {
func localizedLanguage()->String?{
var defaultLanguage = "en"
if let selectedLanguage = UserDefaults.standard.string(forKey: languageKey){
defaultLanguage = selectedLanguage
}
return NSLocalizedString(self, tableName: defaultLanguage, comment: "")
}
}
class LanguageLabel: UILabel{
required init?(coder: NSCoder) {
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: AppNotification.changeLanguage, object: nil)
}
#IBInspectable var localizedLanguage: String? {
didSet{
updateUI()
}
}
#objc func updateUI(){
if let string = localizedLanguage {
text = string.localizedLanguage()
}
}
}
class LanguageButton: UIButton{
required init?(coder: NSCoder) {
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: AppNotification.changeLanguage, object: nil)
}
#IBInspectable var localizedLanguage: String? {
didSet{
updateUI()
}
}
#objc func updateUI(){
if let string = localizedLanguage {
setTitle(string.localizedLanguage(), for: .normal)
}
}
}
struct AppNotification{
static let changeLanguage = Notification.Name("changeLanguage")
}
extension UIViewController{
func changeLanguage(){
let alert = UIAlertController(title: "Change Language", message: "Change it", preferredStyle: .alert)
let actionEnglish = UIAlertAction(title: "English", style: .default) { (action) in
UserDefaults.standard.setValue("en", forKey: languageKey)
NotificationCenter.default.post(name: AppNotification.changeLanguage , object: nil)
}
let actionMontenegrin = UIAlertAction(title: "Montenegrinish", style: .default) { (action) in
UserDefaults.standard.setValue("dutch", forKey: languageKey)
NotificationCenter.default.post(name: AppNotification.changeLanguage , object: nil)
}
alert.addAction(actionEnglish)
alert.addAction(actionMontenegrin)
present(alert, animated: true, completion: nil)
}
}

Refresh Storyboard viewcontroller using swift iOS

Im having button in all viewcontrollers to change language
LanguageViewController.swift
class LanguageViewController: UIViewController {
#IBAction func actionChange(_ sender: Any) {
L102Language.currentAppleLanguage()
L102Language.setAppleLAnguageTo(lang: "en")
// below code to refresh storyboard
self.viewDidLoad()
}
}
L102Language.swift
class func currentAppleLanguage() -> String{
let userdef = UserDefaults.standard
let langArray = userdef.object(forKey: APPLE_LANGUAGE_KEY) as! NSArray
let current = langArray.firstObject as! String
let endIndex = current.startIndex
let currentWithoutLocale = current.substring(to: current.index(endIndex, offsetBy: 2))
return currentWithoutLocale
}
/// set #lang to be the first in Applelanguages list
class func setAppleLAnguageTo(lang: String) {
let userdef = UserDefaults.standard
userdef.set([lang,currentAppleLanguage()], forKey: APPLE_LANGUAGE_KEY)
userdef.synchronize()
}
I inherited LanguageViewController in all my FirstViewCOntroller, SecondController as below
class FirstViewController: LanguageViewController {
}
class SecondController: LanguageViewController {
}
If I call self.viewDidLoad() it fails to change language from view defined in storyboard. How to reload storyboard, so that the language should change in all viewcontroller,if any button from any viewcontroller is clicked? Thanks!
You can use NotificationCenter for reloading the view controllers content, this will also reload the content of view controllers that are not visible.
extension Notification.Name {
static let didChangeLanguage = Notification.Name("didChangeLanguage")
}
override func viewDidLoad() {
//Add a listener
NotificationCenter.default.addObserver(self, selector: #selector(onDidChangeLanguage(_:)), name: .didChangeLanguage, object: nil)
}
#IBAction func actionChange(_ sender: Any) {
L102Language.currentAppleLanguage()
L102Language.setAppleLAnguageTo(lang: "en")
// Notify about the change.
NotificationCenter.default.post(name: .didChangeLanguage, object: self, userInfo: nil)
}
#objc func onDidChangeLanguage(_ notification:Notification) {
// reload content using selected language.
}
Correct me if I'm wrong. but I think you don't need to reload all view controllers. you just need to update them when they get displayed, view controllers are behind the presented one are not visible for the user.
for doing that you can do something like this:
var currentLanguage = ""
override func viewDidLoad() {
currentLanguage = currentAppleLanguage()
loadContentForLanguage(currentLanguage)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this will be executed every time this sceen gets display
if currentLanguage != currentAppleLanguage() {
currentLanguage = currentAppleLanguage()
loadContentForLanguage(currentLanguage)
}
}
func loadContentForLanguage(_ currentLanguage: String) {
//here it goes whatever you currently have in viewDidLoad
}
My apologies if this does not compile, my swift is really rusty.

NSBundleResourceRequest No Progress Update

When I try to observe the progress of a NSBundleResourceRequest, observeValue(forKeyPath: object: change: context:) is not called for the .new observing option. Therefore, the NSProgressIndicator isn't updated. Here is the setup and code:
Setup:
Xcode 8.3.1
Deployment Target iOS 10.3
Device: iPad 4
Resource tags (368 KB) are located located in Download Only On Demand and consist of thumbnails displayed in a UICollectionView. Thumbnail images are located in MyCollection.xcassets. All IBOutlets are connected.
Images are correctly displayed in the collection view, but progress bar remains at zero.
Code:
final class MyCollectionVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var myCollectionVCResourceRequest: NSBundleResourceRequest!
var myCollectionVCResourceRequestLoaded = false
static var myCollectionProgressObservingContext = UUID().uuidString
private let designThumbnailTags: Set<String> = ["Resource1", "Resource2", "Resource3"]
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadOnDemandResources()
}
func loadOnDemandResources() {
myCollectionVCResourceRequest = NSBundleResourceRequest(tags: designThumbnailTags)
myCollectionVCResourceRequest.progress.addObserver(self, forKeyPath: "fractionCompleted", options: [.new, .initial], context: &MyCollectionVC.myCollectionProgressObservingContext)
myCollectionVCResourceRequest.beginAccessingResources(completionHandler: { (error) in
print("Complete: \(self.myCollectionVCResourceRequest.progress.fractionCompleted)") // Prints Complete: 0.0
self.myCollectionVCResourceRequest.progress.removeObserver(self, forKeyPath: "fractionCompleted", context: &MyCollectionVC.myCollectionProgressObservingContext)
OperationQueue.main.addOperation({
guard error == nil else { self.handleOnDemandResourceError(error! as NSError); return }
self.myCollectionVCResourceRequestLoaded = true
self.updateViewsForOnDemandResourceAvailability()
self.fetchDesignThumbnailsWithOnDemandResourceTags(self.myCollectionVCResourceRequest) // Correctly creates Core Data Instances
})
})
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &MyCollectionVC.myCollectionProgressObservingContext {
OperationQueue.main.addOperation({
let progressObject = object as! Progress
self.progressView.progress = Float(progressObject.fractionCompleted)
print(Float(progressObject.fractionCompleted)) // Prints 0.0 as a result of including the .initial option
self.progressDetailLabel.text = progressObject.localizedDescription
})
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

How to know when NSHashTable changed count?

I've tried with KVO but looks like did anything wrong.
My code below
class A : NSObject {
var s: String?
init(s: String) {
super.init()
self.s = s
print("\(self.s) init")
}
deinit {
print("\(self.s) deinit")
}
}
class B : NSObject {
weak var a:A? {
willSet {
print("\(self.a?.s) willSet a \(newValue?.s)")
}
didSet {
print("\(self.a?.s) didSet a \(oldValue?.s)")
}
}
dynamic var hashTable: NSHashTable = NSHashTable.weakObjectsHashTable()
init(a: A?) {
super.init()
self.a = a
print("\(self) init")
hashTable.addObserver(self, forKeyPath: "count", options: .New, context:nil)
hashTable.addObject(a)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("observe")
}
deinit {
print("\(self) deinit")
}
}
Thank you in advance

Detect volume button press

Volume button notification function is not being called.
Code:
func listenVolumeButton(){
// Option #1
NSNotificationCenter.defaultCenter().addObserver(self, selector: "volumeChanged:", name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)
// Option #2
var audioSession = AVAudioSession()
audioSession.setActive(true, error: nil)
audioSession.addObserver(self, forKeyPath: "volumeChanged", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "volumeChanged"{
print("got in here")
}
}
func volumeChanged(notification: NSNotification){
print("got in here")
}
listenVolumeButton() is being called in viewWillAppear
The code is not getting to the print statement "got in here", in either case.
I am trying two different ways to do it, neither way is working.
I have followed this: Detect iPhone Volume Button Up Press?
Using the second method, the value of the key path should be "outputVolume". That is the property we are observing.
So change the code to,
var outputVolumeObserve: NSKeyValueObservation?
let audioSession = AVAudioSession.sharedInstance()
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {}
outputVolumeObserve = audioSession.observe(\.outputVolume) { (audioSession, changes) in
/// TODOs
}
}
The code above won't work in Swift 3, in that case, try this:
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {
print("some error")
}
audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
print("got in here")
}
}
With this code you can listen whenever the user taps the volume hardware button.
class VolumeListener {
static let kVolumeKey = "volume"
static let shared = VolumeListener()
private let kAudioVolumeChangeReasonNotificationParameter = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
private let kAudioVolumeNotificationParameter = "AVSystemController_AudioVolumeNotificationParameter"
private let kExplicitVolumeChange = "ExplicitVolumeChange"
private let kSystemVolumeDidChangeNotificationName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
private var hasSetup = false
func start() {
guard !self.hasSetup else {
return
}
self.setup()
self.hasSetup = true
}
private func setup() {
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
return
}
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.clipsToBounds = true
rootViewController.view.addSubview(volumeView)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.volumeChanged),
name: kSystemVolumeDidChangeNotificationName,
object: nil
)
volumeView.removeFromSuperview()
}
#objc func volumeChanged(_ notification: NSNotification) {
guard let userInfo = notification.userInfo,
let volume = userInfo[kAudioVolumeNotificationParameter] as? Float,
let changeReason = userInfo[kAudioVolumeChangeReasonNotificationParameter] as? String,
changeReason == kExplicitVolumeChange
else {
return
}
NotificationCenter.default.post(name: "volumeListenerUserDidInteractWithVolume", object: nil,
userInfo: [VolumeListener.kVolumeKey: volume])
}
}
And to listen you just need to add the observer:
NotificationCenter.default.addObserver(self, selector: #selector(self.userInteractedWithVolume),
name: "volumeListenerUserDidInteractWithVolume", object: nil)
You can access the volume value by checking the userInfo:
#objc private func userInteractedWithVolume(_ notification: Notification) {
guard let volume = notification.userInfo?[VolumeListener.kVolumeKey] as? Float else {
return
}
print("volume: \(volume)")
}
import AVFoundation
import MediaPlayer
override func viewDidLoad() {
super.viewDidLoad()
let volumeView = MPVolumeView(frame: CGRect.zero)
for subview in volumeView.subviews {
if let button = subview as? UIButton {
button.setImage(nil, for: .normal)
button.isEnabled = false
button.sizeToFit()
}
}
UIApplication.shared.windows.first?.addSubview(volumeView)
UIApplication.shared.windows.first?.sendSubview(toBack: volumeView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
do { try AVAudioSession.sharedInstance().setActive(true) }
catch { debugPrint("\(error)") }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
do { try AVAudioSession.sharedInstance().setActive(false) }
catch { debugPrint("\(error)") }
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let key = keyPath else { return }
switch key {
case "outputVolume":
guard let dict = change, let temp = dict[NSKeyValueChangeKey.newKey] as? Float, temp != 0.5 else { return }
let systemSlider = MPVolumeView().subviews.first { (aView) -> Bool in
return NSStringFromClass(aView.classForCoder) == "MPVolumeSlider" ? true : false
} as? UISlider
systemSlider?.setValue(0.5, animated: false)
guard systemSlider != nil else { return }
debugPrint("Either volume button tapped.")
default:
break
}
}
When observing a new value, I set the system volume back to 0.5. This will probably anger users using music simultaneously, therefore I do not recommend my own answer in production.
If interested here is a RxSwift version.
func volumeRx() -> Observable<Void> {
Observable<Void>.create {
subscriber in
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
} catch let e {
subscriber.onError(e)
}
let outputVolumeObserve = audioSession.observe(\.outputVolume) {
(audioSession, changes) in
subscriber.onNext(Void())
}
return Disposables.create {
outputVolumeObserve.invalidate()
}
}
}
Usage
volumeRx()
.subscribe(onNext: {
print("Volume changed")
}).disposed(by: disposeBag)

Resources