Localize storyboard programatically on the fly - ios

My app allows user to change country, so if they change to Thailand, I am supposed to update my storyboard etc's language.
P/S: I've seen a lot of people say if you relaunch the app you'll be fine, but that's really a user experience issue I wanted to prevent.
The following is my current attempt, it works fine for programatically created/modified/defined text but it doesn't update the text on my storyboard :(
import UIKit
class LocalizationHelper: NSObject {
static var bundle: NSBundle?
static let sharedInstance = LocalizationHelper()
func localizedStringForKey(key: String, comment: String) -> String {
return (LocalizationHelper.bundle?.localizedStringForKey(key, value: comment, table: nil))!
}
func setLanguage(language: String) {
NSUserDefaults.standardUserDefaults().setObject(language, forKey: "LocalizedStringUserDefaultsKey")
NSUserDefaults.standardUserDefaults().synchronize()
if let path = NSBundle.mainBundle().pathForResource(language, ofType: "lproj") {
print(language)
print(language)
LocalizationHelper.bundle = NSBundle(path: path)
print(LocalizationHelper.bundle)
} else {
NSUserDefaults.standardUserDefaults().setObject("Base", forKey: "LocalizedStringUserDefaultsKey")
NSUserDefaults.standardUserDefaults().synchronize()
LocalizationHelper.bundle = NSBundle.mainBundle()
}
}
}
The print statement result is
Optional(NSBundle </var/mobile/Containers/Bundle/Application/91776A02-BADA-4EC9-A451-45A453B84C83/Appname.app/th.lproj> (not yet loaded))
Does this matter anything?
For the record, I referenced this
https://github.com/jslim89/JSLocalizedString
P/S: I have also have my Main.string(Thai) in my Main.storyboard(Base) ready, just that they only update when I change System Language :(

I believe you have outlets defined for each UI component in storyboard. You can simply localize them by calling NSLocalizedString:
self.title = NSLocalizedString("MyTitle", nil);
The other option could be to have multiple storyboard one for each supported language. Once user change the country, based on the supported language pick the right storyboard. Something like this:
let storyboard = UIStoryboard(name: "MainStoryboard_English", bundle: nil)

Related

Back button and creating new entry causes app to crash (UUID cannot be amended) Realm Swift

When I go back to VC1 (which allows the user to input a title for a book and create an entry in realm including the title and a UUID for that book) from VC2 (using the provided back button as part of the navigation controller not a custom segue) and then create a new book object in Realm (by adding another title to the text field in VC1), the app crashes saying I cannot amend the primary key once set.
I am intending to create a new entry (in theory I could add one, go back, add another etc) rather than try to overwrite an existing entry.
I've read the docs (and even looked at an old project where a similar thing is working) but I can't understand why it isn't just creating a new entry. I looked at the Realm docs (e.g. referenced in this answer Realm in IOS: Primary key can't be changed after an object is inserted)
Code here is VC1 allowing the user to create a new novel (by adding a title into a text field which is earlier in the code)
func createNewNovel() {
let realm = try! Realm()
novelCreated.novelID = UUID().uuidString
novelCreated.novelTitle = novelTitleInput.text!
novelCreated.createdDate = Date()
do {
try realm.write {
realm.add(novelCreated)
print ("Novel Created Successfully")
}
} catch {
print("error adding novel")
}
Then I prepare to pass the novelID to VC2 :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let novelData = NovelObject()
novelData.novelID = novelCreated.novelID
if let destVC = segue.destination as? WriteVC {
destVC.novelIDPassed = novelData.novelID
}
}
This works fine and the segue triggers from a button press on VC1. I can also print the ID of the novel in VC2 so that's working fine.
BUT... when I go back from VC2 and input a new title in the text field in VC1 it should create a new NovelObject, not try to update the existing one (which is causing it to crash). You could even theoretically have the same book title twice but they should have a different UUID (which is the primary key)
I must be missing something trivial I suppose but can't work it out!
The novel is created as at the top :
class NewNovelVC: UIViewController {
let novelCreated = NovelObject()
#IBOutlet weak var novelTitleInput: UITextField!
#IBOutlet weak var createButtonOutlet: UIButton!
then it is populated with variables
This is due to classic issue of same object being in the memory re-instantiated which points to the same value of primary key. Creating a singleton class can be very handy here as well.
Create a service file. This will keep your RealSwift initialized on a specific thread as your current block.
RealService.swift
import RealmSwift
class RealmService {
static let uirealm = RealmService()
private var _initRS = try! Realm()
var realm: Realm! {
return _initRS
}
}
Novel.swift :
import RealmSwift
class Novel : Object {
#objc dynamic var uid : String? = nil
#objc dynamic var title: String? = nil
override static func primaryKey() -> String {
return "uid"
}
}
extension Novel {
func writeToRealm(){
try? RealmService.uirealm.realm.write {
print("Creating the Novel Object")
RealmService.uirealm.realm.add(self, update: true)
}
}
func DeleteFromRealm(object: Results<Novel>){
try? RealmService.uirealm.realm.write {
print("Deleting the Novel Object")
RealmService.uirealm.realm.delete(object)
}
}
}
and just implement as #Don mentioned, write in the block where you are setting the title.
var novel: Novel! // Declare this in above in the class.
novel = Novel()
novel.title = title
novel.uid = uid
novel.writeToRealm()
Hope that helped.

How can I get array of UIStoryboard in Swift?

I find that my Storyboard has become very complex and decided to split it into different Storyboards. But I want to have the freedom to instantiate a UIViewController no matter where I put the view controller in. That way, I can move around View Controller from Storyboard to Storyboard without the need to remember where did I put that View Controller, and I also don't have to update the code at all because they all use the same code to instantiate the same View Controller with that name, no matter where it resides.
Therefore, I want to create an extension of UIViewController like this:
extension UIViewController {
func instantiate (named: String?, fromStoryboard: String? = nil) -> UIViewController? {
guard let named = named else { return nil; }
if let sbName = fromStoryboard {
let sb = UIStoryboard(name: sbName, bundle: nil);
return sb.instantiateViewController(withIdentifier: named);
}
else {
for sb in UIStoryboard.storyboards {
if let vc = sb.instantiateViewController(withIdentifier: named) {
return vc;
}
}
}
return nil;
}
The problem is, I cannot find the property / method to return the list of storyboard instances like .storyboards anywhere. Is there any workaround on this? I know that I can probably have a static list of storyboard names, but that way, the extension won't be dynamic and independent of the projects.
Can anybody help? Thanks.
EDIT:
Combining the accepted answer and answer from here to safely instantiate viewcontroller (and return nil if not found), this is my code:
UIStoryboard+Storyboards.swift:
extension UIStoryboard {
static var storyboards : [UIStoryboard] {
let directory = Bundle.main.resourcePath! + "/Base.lproj"
let allResources = try! FileManager.default.contentsOfDirectory(atPath: directory)
let storyboardFileNames = allResources.filter({ $0.hasSuffix(".storyboardc" )})
let storyboardNames = storyboardFileNames.map({ ($0 as NSString).deletingPathExtension as String })
let storyboardArray = storyboardNames.map({ UIStoryboard(name: $0, bundle: Bundle.main )})
return storyboardArray;
}
func instantiateViewControllerSafe(withIdentifier identifier: String) -> UIViewController? {
if let availableIdentifiers = self.value(forKey: "identifierToNibNameMap") as? [String: Any] {
if availableIdentifiers[identifier] != nil {
return self.instantiateViewController(withIdentifier: identifier)
}
}
return nil
}
}
UIViewController+Instantiate.swift:
extension UIViewController {
static func instantiate (named: String?, fromStoryboard: String? = nil) -> UIViewController? {
guard let named = named else { return nil; }
if let sbName = fromStoryboard {
let sb = UIStoryboard(name: sbName, bundle: nil);
return sb.instantiateViewControllerSafe(withIdentifier: named);
}
else {
for sb in UIStoryboard.storyboards {
if let vc = sb.instantiateViewControllerSafe(withIdentifier: named) {
return vc;
}
}
}
return nil;
}
}
With the restriction that all the storyboard files must be located on Base.lproj folder.
It's not the most efficient code in terms of running time, I know. But for now, it's easy enough to be understood and I can live with this. :) Thanks for everybody who helps!
Storyboards are normally set up to be localized, so you should look for them in the Base.lproj subdirectory of your bundle's resource directory. A storyboard is compiled into a file with the extension .storyboardc, so you should look for files with that suffix and strip it.
let directory = Bundle.main.resourcePath! + "/Base.lproj"
let allResources = try! FileManager.default.contentsOfDirectory(atPath: directory)
let storyboardFileNames = allResources.filter({ $0.hasSuffix(".storyboardc" )})
let storyboardNames = storyboardFileNames.map({ ($0 as NSString).deletingPathExtension as String })
Swift.print(storyboardNames)
If you have created device-specific storyboards (by adding ~ipad or ~iphone to the storyboard filenames) then you'll also want to strip off those suffixes and eliminate duplicates.
Note that the compiled storyboard suffix in particular is not a documented part of the SDK, so it could change in a future version of iOS / Xcode.
Don't do that.
Instead, use Storyboard References to segue to different storyboards as necessary.
In your situation, maybe it would be cleaner if you discard storyboard all together. And create all your view controllers programmatically.
As stated elsewhere storyboards are compiled to opaque (i.e. binary, undocumented) .storyboardc files when the app is compiled and run. The UIStoryboard API only allows instantiating the initial View Controller, or a (known) named one, so there's no naive way to interrogate your app for 'unknown' storyboard elements. There may also be side-effects to instantiating UI elements that are not being displayed. However...
If you put your .storyboard files in a Copy Files build phase you can interrogate the XML and recover e.g. UIViewController/UIView identifiers, custom properties etc. IBDecodable seems to do a reasonable job of this. Installation-wise (cocoapods) it's MacOS-only but will run happily if you embed it in your iOS app (install the SWXMLHash prerequisite and clone IBDecodable/git-submodule it, whatever). There are probably efficiency and localisation issues I'm skimping on but for my use case (building onboarding popovers from custom properties without being explicit about it) it worked OK.
To answer the question posed more specifically, interrogating the storyboards for IDs would allow the app to find (via a dict or similar), and instantiate a View Controller, wherever it was visually stored.
For example:
Bundle.main.urls(forResourcesWithExtension: "storyboard", subdirectory: nil)?
.forEach({ (url) in
do {
let file = try StoryboardFile(url: url)
if !file.document.launchScreen {
file.document.scenes?.forEach({ (scene) in
scene.viewController?.viewController.rootView?.subviews?
.forEach({ (view) in
// Do stuff...
})
})
}
} catch { /* ... */ }
})

Prevent runtime crash for function instantiateViewController(withIdentifier:)

Swift 3.0
From what I found here is UIStoryboard always return non-optional instance in function instantiateViewController(withIdentifier:).
open class UIStoryboard : NSObject {
...
open func instantiateViewController(withIdentifier identifier: String) -> UIViewController
}
The crash happen if we adding wrong identifier value, without noticed.
This case might happen in complex projects which have large number of controller in many storyboards, and a controller StoryboardId is missed.
The only solution I found is making a function to temporary create all controllers when start app delegate (this function should be called in debugging mode only).
func validateControllers() {
guard let _ = xyzStoryboard.instantiateViewController(withIdentifier: "ABC") as? ABCViewController else {
fatalError("fail in init controller ABC from storyboard XYZ")
}
...
}
But I wonder if we could have another way to handle this situation. Or should I raise this issue to Swift team? Thanks!
as far as i know there is no solution for preventing crashes like this. but this is a good thing and it's meant to crash like this to show you there is something wrong with your code and you need to fix them!
Writing this answer to just to let you know how we handled the accidental typo(s) of a View Controller Identifier, which could lead to error when you try to create an ViewController from Storyboard(s) using view controller's identifier.
We had a complex project which had almost 15-20 ViewControllers, we didn't put them in a single storyboard instead we shared these VCs across multiple story boards and then we created an object called StoryBoardManager, which would create an VC from various storyboard(s) and hand it over to us.
We also created couple of enums to represent various storyboard(s) and viewController(s) inside.
It somewhat looks like this,
enum Storyboards : String {
case main = "Main"
case signup = "SignUp"
case discover = "Discover"
case utility = "Utility"
case event = "Event"
}
enum ViewControllers : String {
case login = "login"
case onBoard = "on_board"
case signup = "signup"
case signupNavigation = "signupNavigaitonVC"
case discoverNavigation = "discoverNavigation"
case individualProfileSetUp = "individualProfileSetUp"
case organizationProfileSetUp = "organizationProfileSetUp"
case discover = "discover"
case imagePickerMenuVC = "imagePickerMenuVC"
case eventDiscoverMapNavigation = "eventDiscoverMapNavigationVC"
case eventDiscoverMapVC = "eventDiscoverMapVC"
case event = "event"
}
class StoryboardManager {
static func storyboard(name:Storyboards) -> UIStoryboard {
let aStoryboard = UIStoryboard(name: name.rawValue, bundle: nil)
return aStoryboard
}
static func viewController(storyboardId:Storyboards, viewControllerId:ViewControllers) -> UIViewController {
let storyboard = StoryboardManager.storyboard(storyboardId)
let viewcontroller = storyboard.instantiateViewControllerWithIdentifier(viewControllerId.rawValue)
return viewcontroller
}
}
We mainly did this to avoid the typo mistakes for the ViewController identifier(s), which would lead to an runtime error. You will add all the viewController(s) identifiers to ViewControllers enum and storyboard(s) names to the Storyboards enum as a enum case. We updated both the enums whenever we introduced a new storyboard or a new ViewController in any of the storyboards and this helped all the team members not to make the typo for ViewController identifier.
Then we created ViewController(s) using the below code,
let loginVC = StoryboardManager.viewController(.main, viewControllerId: .login)
HTH :)
Short brief
Currently, I have a complex project that have 5 storyboards, with 20 controllers. In my case, using storyboard segue seems not a good idea. Therefore, creating controller's instance from storyboard and make a transition by code is much better.
But the problem is that instantiateViewController(withIdentifier:) always return non-optional instance, and it will be crashed if we put an invalid identifier (which didn't define in storyboard).
Solution (Swift 3.0)
There are steps to prevent this problem.
In *.storyboard, use controller class name as StoryboardID
Create UIStoryboard+Ext.swift to define all storyboards & controllers belong
Create BaseViewController.swift to define the initialization from storyboard
Create function to init all storyboards & controllers, to prevent crashing in runtime.
Download sample code from if you want to try yourself: https://github.com/nahung89/StoryboardPattern
Detail (TL;DR)
Step 1:
Step 2:
extension UIStoryboard {
enum Identifier: String {
case main = "Main"
case explore = "Explore"
case search = "Search"
case profile = "Profile"
}
static func name(for controller: UIViewController.Type) -> String? {
var identifier: Identifier?
switch controller.className {
case HomeViewController.className:
identifier = .main
case ExploreViewController.className:
identifier = .explore
case SearchViewController.className:
identifier = .search
case ProfileViewController.className:
identifier = .profile
default:
break
}
return identifier?.rawValue
}
}
extension UIStoryboard {
static func instanceFromIdentifier(_ identifier: Identifier) -> UIStoryboard {
return UIStoryboard(name: identifier.rawValue, bundle: Bundle.main)
}
static func instanceFromName(_ name: String) -> UIStoryboard? {
guard let identifier = Identifier(rawValue: name) else { return nil }
return instanceFromIdentifier(identifier)
}
}
Step 3:
extension UIViewController {
var className: String {
return String(describing: type(of: self))
}
class var className: String {
return String(describing: self)
}
}
class BaseViewController: UIViewController {
static var controllerId: String {
return String(describing: self) // return slass name, i.e "ExploreViewController"
}
static func instanceFromStoryboard() -> Self? {
guard let storyboardName = UIStoryboard.name(for: self) else { return nil }
return instantiateFrom(storyboardName: storyboardName)
}
private static func instantiateFrom<VC: UIViewController>(storyboardName: String) -> VC? {
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: controllerId) as? VC
return controller
}
}
Step 4
extension AppDelegate {
func validateStoryboards() {
guard
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.main.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.explore.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.profile.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.search.rawValue)
else {
fatalError("fail to init storyboard by name")
}
guard let _ = HomeViewController.instanceFromStoryboard(),
let _ = ExploreViewController.instanceFromStoryboard(),
let _ = ProfileViewController.instanceFromStoryboard(),
let _ = SearchViewController.instanceFromStoryboard()
else {
fatalError("fail to init controller from storyboard")
}
}
}

NSUserDefaults messing with SegmentedControl

I have a simple expense tracker app that has at the top a Segmented control to change currency (Romanian currency, Euro and $ in this order). In my list manager I have methods that convert from one to another and in the view controller, based on the Segmented control, I call them accordingly.
My problem is the following: When the app starts, the selected currency is always the first - Ro currency. If I add some elements and then I quit the app with another currency selected, I guess NSUserDefaults synchronizes with those values. When I open the app again, Ro currency is again selected, but with the $/euro values. (Say I add 410 ron, which converted is ~$100. I select $ and kill the app from multitasking. When I open it up again, it show 100 ron, instead of 410). If a currency is selected and add a certain amount, it performs ok (if $ is selected and I add 100, when I switch the control to ron it displays 410). I guess I have to change something with the synchronization, but I can't figure out where and how.
EDIT2: Some code (sorry)
//This is the expenses class
import UIKit
class Expense: NSObject, NSCoding {
//MARK: Properties
var title:String?
var amount:Double?
init(title:String,amount:Double){
self.title = title
self.amount = amount
}
override init(){
super.init()
}
required init(coder aDecoder: NSCoder){
if let titleDecoded = aDecoder.decodeObjectForKey("title") as? String{
title = titleDecoded
}
if let amountDecoded = aDecoder.decodeObjectForKey("amount") as? Double{
amount = amountDecoded
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let titleEncoded = title {
aCoder.encodeObject(titleEncoded, forKey:"title")
}
if let amountEncoded = amount {
aCoder.encodeObject(amountEncoded, forKey:"amount")
}
}
}
//The NSUserDefaultsManager
import UIKit
class NSUserDefaultsManager: NSObject {
static let userDefaults = NSUserDefaults.standardUserDefaults()
class func synchronize(){
let myData = NSKeyedArchiver.archivedDataWithRootObject(ExpenseManager.expenses)
NSUserDefaults.standardUserDefaults().setObject(myData, forKey: "expensearray")
NSUserDefaults.standardUserDefaults().synchronize()
}
class func initializeDefaults(){
if let expensesRaw = NSUserDefaults.standardUserDefaults().dataForKey("expensearray"){
if let expenses = NSKeyedUnarchiver.unarchiveObjectWithData(expensesRaw) as? [Expense]{
ExpenseManager.expenses = expenses
}
}
}
}
I call the initializeDefaults in the tableview manager in view did load, the synchronize in view did appear and in the App Delegate module, sync in applicationWillTerminate.
ANSWER
I found a solution - it was quite obvious. I found it in a Google Books - iOS 9 Programming Fundamentals by Matt Neuburg.
In the segmented control action I added the following
let c = self.currencySelector.selectedSegmentIndex
NSUserDefaults.standardUserDefaults().setObject(c, forKey: "selectedcurrency")
while in viewDidLoad I added
let c = NSUserDefaults.standardUserDefaults().objectForKey("selectedcurrency") as! Int
currencySelector.selectedSegmentIndex = c
sorry I don't have enough reputation to comment. But could you show the code, and Im guessing you're not synchronizing something or just not pulling it up in ViewDidLoad, etc.

Swift after signup move to 2nd or any other tab of UITabBarController

In my App after signup/login I want to move control to specific tab of UITabBarController on some conditions. My login or Signup Pages are not in UITabBarController.
Here is the code of my login button I have tried but it only works with identifier or UITabBarController. In my case I want to move to specific tab like 2nd or 3rd tab.
#IBAction func btn_login(sender: AnyObject) {
activityIndicator.progressBarDisplayer("Logging In", true, uiView: self.view)
let timezone = GLib.timezone();
let parameters = ["email": txtemail.text!,"password": txtpassword.text!,"time_zone": timezone]
GLib.fetch("www.abc.com/test/auth/login", parameters: parameters, localkey: "user"){
(isResponse) -> Void in
if(isResponse == true)
{
self.activityIndicator.strLabel.text = "Personalizing Data";
let viewController:UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("some identifier") as UIViewController;
self.presentViewController(viewController, animated: false, completion: nil);
}else
{
self.show_alert("Attention Please!", message: self.GLib.error)
}
}
I have seen many posts but found nothing similar to my mechanism.
I am new to iOS programming so don't know how to figure out this issue.
Using instantiateViewControllerWithIdentifier is the right way to go. However, you need to add the actual identifier, and the UIViewController Custom class type needs to be the actual class.
I'm going to give you a rough example of what it looks like, but it should work. Either way you have to edit your instantiate method as I explain below.
Steps:
Firstly, set the "Storyboard ID" for the viewController in your storyboard. Like this:
Then add this code modified instantiateViewControllerWithIdentifier method:
#IBAction func btn_login(sender: AnyObject) {
activityIndicator.progressBarDisplayer("Logging In", true, uiView: self.view)
let timezone = GLib.timezone();
let parameters = ["email": txtemail.text!,"password": txtpassword.text!,"time_zone": timezone]
GLib.fetch("www.abc.com/test/auth/login", parameters: parameters, localkey: "user"){
(isResponse) -> Void in
if(isResponse == true)
{
self.activityIndicator.strLabel.text = "Personalizing Data";
let yourNewViewController: ViewControllerClassName = self.storyboard!.instantiateViewControllerWithIdentifier("yourStoryBoardID") as! ViewControllerYourePushingTo
self.presentViewController(yourNewViewController, animated: true, completion: nil)
}else
{
self.show_alert("Attention Please!", message: self.GLib.error)
}
}
As a type, you need to actually use the viewController's name.
I asked a similar question a few weeks ago: Swift: Triggering TableViewCell to lead to a link in a UIWebView in another ViewController

Resources