Change default NSManagedObjectContext of NSPersistentDocument - cocoa

Core data newbie here. I'm trying to change the default NSManagedObjectContext of an NSPersistentDocument, in order to initialise and use it with NSMainQueueConcurrencyType.
Currently I'm doing it in -windowControllerDidLoadNib: like this:
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
[super windowControllerDidLoadNib:aController];
NSManagedObjectContext *newMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[newMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];
[self setManagedObjectContext:newMOC];
}
This seemingly works fine. But I'm wondering if initialisation of the MOC in -windowControllerDidLoadNib: is the best thing to do, or whether it should be placed somewhere else and/or initialised in a different way.
Thanks for any help.

I'm experimenting with the Xcode template for a document-based CoreData app. The template creates an init() override which just calls super.init(). I want to run a large import in the background, so I added this to the document class:
class Document: NSPersistentDocument {
private var importQueue = DispatchQueue(label: "Importer")
override init() {
super.init()
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.mergePolicy = self.managedObjectContext!.mergePolicy
moc.persistentStoreCoordinator = self.managedObjectContext!.persistentStoreCoordinator
self.managedObjectContext = moc
}
func importStuff(url: URL) {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.parent = self.managedObjectContext
var count = 0
moc.performAndWait {
...
count += 1
if count % 10000 == 0 {
do {
try moc.save()
moc.reset()
}
catch {
Swift.print("save failed at record #\(count): \(error.localizedDescription)")
}
}
return true
}
do {
try moc.save()
}
catch {
Swift.print("save failed at records #\(count): \(error.localizedDescription)")
}
}
Swift.print("imported \(count) records.")
}
#IBAction func import(_ sender: Any) {
...
importQueue.async {
self.importStuff(url: url)
}
}
}
This seems to work OK in my initial tests. I think initializing a new MOC in -windowControllerDidLoadNib: is OK, but if you have object controllers bound to the document MOC, they might perform a second fetch when the MOC is changed. Initializing it in the init will initialize it sooner, before the UI is loaded.

The preferred pattern is the "pass-the-baton" approach where you pass the managed object context down to the child view controllers. Give your controllers a managed object context attribute and simply pass it on when you present them.

Related

How Can I Tell If My NSManagedObject Resides in a Read Only NSPersistentStore?

I would like to add read-only example/tutorial data to my Core Data based macOS app.
I will include an SQL file in my application bundle containing the example data. My NSPersistentContainer will have 2 NSPersistentStores, one writable and one read only. I will only have a default configuration for my model since both stores will have the same model.
My UI will need to know if the data displayed is read only or not, for example, to stop this data being draggable.
I know that NSManagedObject does not support a readonly state, see and : Is it possible to return NSManagedObjects as read-only in Core Data? ...and the docs.
I think the best approach would be to add a readonly property to my NSManagedObject derived class that can be queried where necessary. However, I can't see how I could easily set this property! I can't find a direct link to an NSPersistentStore from an NSManagedObject.
I could set up an NSFetchRequest and specify the read only store and see if the NSManagedObject is in it, but that seems a little ridiculous.
Am I missing something more obvious here please?
With thanks to pbasdf for his suggestion...
I could find no straight-forward way to achieve this. I had to move away from using NSPersistentContainer to simplify my Core Data stack. However, I think this is a fairly elegant solution if you need a small subset of your graph to be readonly.
I subclassed NSPersistentStoreCoordinator to cache the NSManagedObjectIDs of any readonly store added to it:
class GraphStoreCoordinator: NSPersistentStoreCoordinator
{
override init(managedObjectModel model: NSManagedObjectModel)
{
readOnlyTestContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
super.init(managedObjectModel: model)
readOnlyTestContext.persistentStoreCoordinator = self
NotificationCenter.default
.addObserver(forName: .NSPersistentStoreCoordinatorStoresDidChange,
object: self, queue: nil) { [unowned self] notification in
// userInfo will be in this form for add/remove keys - not supporting migration here
guard let userInfo = notification.userInfo as? [String: [NSPersistentStore]] else {
unhandledError("Invalid userInfo for NSPersistentStoreCoordinatorStoresDidChange.") }
userInfo[NSAddedPersistentStoresKey]?.forEach { self.didAddStore($0) }
userInfo[NSRemovedPersistentStoresKey]?.forEach { self.didRemoveStore($0) }
}
}
deinit {
NotificationCenter.default
.removeObserver(self, name: .NSPersistentStoreCoordinatorStoresDidChange, object: self)
}
private func didAddStore(_ store: NSPersistentStore) {
guard store.isReadOnly else { return }
var addedObjects = Set<NSManagedObjectID>()
baseEntityNames.forEach { entityName in
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
fetchRequest.affectedStores = [store]
do {
let addedEntityObjects = try readOnlyTestContext.fetch(fetchRequest)
addedObjects = addedObjects.union(addedEntityObjects.map { $0.objectID })
} catch {
unhandledError("Failed to fetch all \(entityName) for read only check: \(error)") }
}
readOnlyObjects[store.identifier] = addedObjects
}
private func didRemoveStore(_ store: NSPersistentStore) {
guard store.isReadOnly else { return }
readOnlyObjects.removeValue(forKey: store.identifier)
}
/// Returns the minimum set of entities that can be fetched for readonly checking
private lazy var baseEntityNames: [String] = {
return managedObjectModel.entitiesByName.compactMap { $1.superentity == nil ? $0 : nil }
}()
private var readOnlyTestContext: NSManagedObjectContext
/// Readonly objectIDs keyed per persistent store
private var readOnlyObjects = [String : Set<NSManagedObjectID>]()
internal func isObjectReadOnly(_ objectID: NSManagedObjectID) -> Bool {
return readOnlyObjects.contains(where: { $1.contains(objectID) } )
}
}
I then added an extension to NSManagedObject to that queries its NSPersistentStoreCoordinator for read-only status:
public extension NSManagedObject
{
/// Does this managed object reside in a read-only persistent store?
var isReadOnly: Bool {
guard let coordinator = managedObjectContext?
.persistentStoreCoordinator as? GraphStoreCoordinator else {
unhandledError("Should only check readonly status in a GraphStoreCoordinator") }
return coordinator.isObjectReadOnly(objectID)
}
}

I'm trying to delete a record out of Core Data in xCode 8/Swift 3 & latest core data syntax

I'm trying to delete an entire record out of coreData. I've retrieved the data and placed it in an array for manipulation (I have another function that lets the user edit the data using this method and it works fine) But I can't figure out how to just delete the record. [.remove(at: index)] doesn't work and neither does the code below. I can set all the fields to empty but that's not what I want, I want the record gone completely.
I went through the solutions given for similar problems but to no avail
#IBAction func Delete(_ sender: UIButton) { // The delete function
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let context = appDelagate.persistentContainer.viewContext
var destArray = [DestinationsOne]() // The data array
do {
try destArray = context.fetch(request) as! [DestinationsOne]} //Fetching the data and placing it in the array
catch{
//error message
}
for index in (0..<destArray.count - 1){ //Go through the records
if destArray[index].destID == IDTitle!{ //Picks the record to edit
let object = destArray[index]
context.delete(object
}
appDelagate.saveContext()
}
I figured this one out. I'm posting the solution in case anyone else has the same question
func deleteRecords() -> Void { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let result = try? moc.fetch(fetchRequest)
let resultdata = result as! [DestinationsOne] // result as entity
for object in resultdata { // Go through the fetched result
if object.destID == self.IDTitle{ // If there is a match
moc.delete(object) // delete the object
}
}
do {
try moc.save() // Save the delete action
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
}
func getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
deleteRecords() // Call the function
Why not applying a predicate to search this particular record. It's much more efficient than looping through a huge list.
func deleteRecords() { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<DestinationsOne>(entityName: "DestinationsOne")
let predicate = NSPredicate(format: "destID == %#", self.IDTitle)
fetchRequest.predicate = predicate
do {
let resultdata = try moc.fetch(fetchRequest) // no type cast needed
if let objectToDelete = resultdata.first {
moc.delete(objectToDelete) // delete the object
try moc.save() // Save the delete action
}
} catch {
print("Could not save error: ", error)
}
}
Here are some issues with your code:
viewContext should be treated as readonly - you should use performBackgroundTask for all changes to core-data
You are fetching ALL of the entities and then then going through each one to find the one you want to delete. It is a lot faster to have core-data only fetch the one you want. You can do this by setting a predicate to the fetch request.
Instead of displaying your records by doing a fetch and using the array as a model, it is better to use a NSFetchedResultsController to do the fetch and manage the results. The fetchedResultsController will keep the data in sync when objects are changed, inserted or deleted. It also has delegate methods that will inform you when there are changes so you can update your view.
remove appDelagate.saveContext from your project. Apple's template code is wrong. You should never be writing to the viewContext so you should never have a reason to save it.
where is IDTitle being set? are you sure it is not nil?
(minor) for index in (0..<destArray.count - 1){ can be replaced with for (index, element) in destArray.enumerated() { which is clearer to read.

Passing Dictionary to Watch

I'm trying to pass data from iPhone -> Watch via Watch Connectivity using background transfer via Application Context method.
iPhone TableViewController
private func configureWCSession() {
session?.delegate = self;
session?.activateSession()
print("Configured WC Session")
}
func getParsePassData () {
let gmtTime = NSDate()
// Query Parse
let query = PFQuery(className: "data")
query.whereKey("dateGame", greaterThanOrEqualTo: gmtTime)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objectsFromParse = objects as? [PFObject]{
for MatchupObject in objectsFromParse
{
let matchupDict = ["matchupSaved" : MatchupObject]
do {
try self.session?.updateApplicationContext(matchupDict)
print("getParsePassData iPhone")
} catch {
print("error")
}
}
}
}
}
}
I'm getting error twice printed in the log (I have two matchups in Parse so maybe it knows there's two objects and thats why its throwing two errors too?):
Configured WC Session
error
error
So I haven't even gotten to the point where I can print it in the Watch app to see if the matchups passed correctly.
Watch InterfaceController:
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
let matchupWatch = applicationContext["matchupSaved"] as? String
print("Matchups: %#", matchupWatch)
}
Any ideas? Will post any extra code that you need. Thanks!
EDIT 1:
Per EridB answer, I tried adding encoding into getParsePassData
func getParsePassData () {
let gmtTime = NSDate()
// Query Parse
let query = PFQuery(className: "data")
query.whereKey("dateGame", greaterThanOrEqualTo: gmtTime)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objectsFromParse = objects as? [PFObject]{
for MatchupObject in objectsFromParse
{
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
let matchupDict = ["matchupSaved" : data]
do {
try self.session?.updateApplicationContext(matchupDict)
print("getParsePassData iPhone")
} catch {
print("error")
}
}
}
}
}
}
But get this in the log:
-[PFObject encodeWithCoder:]: unrecognized selector sent to instance 0x7fbe80d43f30
*** -[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
EDIT 2:
Per EridB answer, I also tried just pasting the function into my code:
func sendObjectToWatch(object: NSObject) {
//Archiving
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
//Putting it in the dictionary
let matchupDict = ["matchupSaved" : data]
//Send the matchupDict via WCSession
self.session?.updateApplicationContext(matchupDict)
}
But get this error on the first line of the function:
"Use of unresolved identifer MatchupObject"
I'm sure I must not be understanding how to use EridB's answer correctly.
EDIT 3:
NSCoder methods:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
//super.init(coder: aDecoder)
configureWCSession()
// Configure the PFQueryTableView
self.parseClassName = "data"
self.textKey = "matchup"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
Error
You are getting that error, because you are putting a NSObject (MatchupObject) which does not conform to NSCoding inside the dictionary that you are going to pass.
From Apple Docs
For most types of transfers, you provide an NSDictionary object with
the data you want to send. The keys and values of your dictionary must
all be property list types, because the data must be serialized and
sent wirelessly. (If you need to include types that are not property
list types, package them in an NSData object or write them to a file
before sending them.) In addition, the dictionaries you send should be
compact and contain only the data you really need. Keeping your
dictionaries small ensures that they are transmitted quickly and do
not consume too much power on both devices.
Details
You need to archive your NSObject's to NSData and then put it in the NSDictionary. If you archive a NSObject which does not conform to NSCoding, the NSData will be nil.
This example greatly shows how to conform a NSObject to NSCoding, and if you implement these things then you just follow the code below:
//Send the dictionary to the watch
func sendObjectToWatch(object: NSObject) {
//Archiving
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
//Putting it in the dictionary
let matchupDict = ["matchupSaved" : data]
//Send the matchupDict via WCSession
self.session?.updateApplicationContext(matchupDict)
}
//When receiving object from the other side unarchive it and get the object back
func objectFromData(dictionary: NSDictionary) -> MatchupObject {
//Load the archived object from received dictionary
let data = dictionary["matchupSaved"]
//Deserialize data to MatchupObject
let matchUpObject = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! MatchupObject
return matchUpObject
}
Since you are using Parse, modifying an object maybe cannot be done (I haven't used Parse in a while, so IDK for sure), but from their forum I found this question: https://parse.com/questions/is-there-a-way-to-serialize-a-parse-object-to-a-plain-string-or-a-json-string which can help you solve this problem easier than it looks above :)

Whats the easiest way to empty and save a Core Data Table in Swift?

So I have a entity, which I am treating as a simple Database Table in iOS8 XCODE and SWIFT. I want to delete every entry in the table. Permanently. So when I start up the app again they do not reload.This is my code.
func deleteAllItems(){
println("All Items are being DELETED")
var count:Int = 0
while (HBCContactList.count > 0){
let AppDel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let MOContext: NSManagedObjectContext = AppDel.managedObjectContext!
MOContext.deleteObject(HBCContactList[0] as NSManagedObject)
HBCContactList.removeAtIndex(0)
var error:NSError? = nil
if !MOContext.save(&error){
abort()
}
tableView.reloadData()
}
It looks like it loads. When its finished on the screen there is nothing in UITableView and all is good. However If I write code to return me the entity via a fetch statement and do a count on the number of records. It still says there is over 150 results.
Thoughts? Am I even in the right ball park?
One solution is to fetch the objects and delete them.
Here is an example (make sure you specify your own entity) :
// If you'll be using the Managed Object Contexte often,
// you might want to make it a lazy property :
lazy var managedObjectContext : NSManagedObjectContext? = {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if let managedObjectContext = appDelegate.managedObjectContext {
return managedObjectContext
}
else {
return nil
}
}()
func deleteData() {
let context = self.managedObjectContext!
let fetchRequest = NSFetchRequest(entityName: "yourEntity")
fetchRequest.includesPropertyValues = false // Only fetch the managedObjectID (not the full object structure)
if let fetchResults = context.executeFetchRequest(fetchRequest, error: nil) as? [yourEntity] {
for result in fetchResults {
context.deleteObject(result)
}
}
var err: NSError?
if !context.save(&err) {
println("deleteData - Error : \(err!.localizedDescription)")
abort()
} else {
println("deleteData - Success")
}
}

Storyboard/XIB and localization best practice

The officially recommended method for XIB/Storyboard localization is to create .xib and .storyboard files inside xx.lproj (where xx is the two letter language ID) for each localization you want to support.
This creates a problem because you have multiple files that in many cases share the same UI, that are prone to change. If you wanted to re-design the UI for one view, you'll have to do it multiple times (worse if you entered the localizable string values in the xib itself). This goes against the DRY principle.
It seems way more efficient to call NSLocalizedString() where you need it, and just use one XIB or Storyboard for one base localization.
So, why should(n't) I create localized XIB/Storyboard files?
You can make a category on UILabel, UIButton etc. like this:
#import "UILabel+Localization.h"
#implementation UILabel (Localization)
- (void)setLocalizeKey:(NSString*)key
{
self.text = NSLocalizedString(key, nil);
}
#end
and after that on your xib file use User Defined Runtime Attributes to link the UILabel (or UIButton etc.) to a key saved in your Localizable.strings file
This way you can have all your strings in one file and you do not have to create a separate xib for each language.
For just changing text labels I did something like this
+(void) replaceTextWithLocalizedTextInSubviewsForView:(UIView*)view
{
for (UIView* v in view.subviews)
{
if (v.subviews.count > 0)
{
[self replaceTextWithLocalizedTextInSubviewsForView:v];
}
if ([v isKindOfClass:[UILabel class]])
{
UILabel* l = (UILabel*)v;
l.text = NSLocalizedString(l.text, nil);
[l sizeToFit];
}
if ([v isKindOfClass:[UIButton class]])
{
UIButton* b = (UIButton*)v;
[b setTitle:NSLocalizedString(b.titleLabel.text, nil) forState:UIControlStateNormal];
}
}
}
call this function in your viewDidLoad: like this:
[[self class] replaceTextWithLocalizedTextInSubviewsForView:self.view];
It saved me a lot of work declaring and connecting IBOutlets when all you want is localized labels.
Flax's solution works well, one thing to note is that if you have UILabels or UIButtons which are are contained in UICollectionViewCells in UICollectionViews (or similar) and these collections change frequently in the current view, eg due to user action or being populated by an asynchronous request, then to keep the labels updated with the correct localization strings you can call the localization function in viewDidLayoutSubviews instead of viewDidLoad (which is called only once):
- (void)viewDidLayoutSubviews
{
[LocalizationHelper replaceTextWithLocalizedTextInSubviewsForView:self.view];
}
As can be seen from this code, I keep the localization method in a static helper class (as the other chap suggested):
#implementation LocalizationHelper
+(void) replaceTextWithLocalizedTextInSubviewsForView:(UIView*)view
{
for (UIView* v in view.subviews)
... <code from above> ...
}
#end
Would have added this as a comment to the above solution, but I ain't got no 'rep!
As explained by Leszek S you can create a category.
Here I'll give you an example in swift 3 with extension for UILabel and UIButton:
First of all create a "StringExtension.swift" file
Add on it this code:
extension String {
func localized() -> String {
let bundle = Bundle.main
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
}
}
Then create another new file with the name you want (for example) "LocalizableObjectsExtensions.swift"
Add on it an extension for UIButton and one for UILabel like this (of course you can create extension for what you want, UITextField...):
extension UIButton {
var localizedText: String {
set (key) { setTitle(key.localized(), for: .normal) }
get { return titleLabel!.text! }
}
}
extension UILabel {
var localizedText: String {
set (key) { text = key.localized() }
get { return text! }
}
}
Now go in your Storyboard and for your button and/or you label that you want localize just add in the identity inspector of you object this:
FYI: here Key Path it's the name of the function you added in your extensions (UIlabel and UIButton) and Value is the name of the key that you want translate automatically which is in your Localizable.strings file. For example in your Localizable.strings (French) you have the key/value "ourOffers" = "NOS OFFRES";
Now build & Run. Your Object will be translated in the language of your device if you have the key/value in your Localizable.string. Enjoy :)
you can automate a lot of it with ibtool. this is a decent introduction: http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/
Every place I look says that you have to replicate the entire xib file for each localization instance, even though you really only wanted to rip the text out and replicate the text in a different language for each localization instance.
If anyone knows of a method to replicate only the user visible text of an xib (in a different language) without replicating the entire xib file for each language, please let us know.
Useful post, much easier than multiple XIBs. I extended the code to handle UISegmentedControl:
if ([v isKindOfClass:[UISegmentedControl class]]) {
UISegmentedControl* s = (UISegmentedControl*)v;
for (int i = 0; i < s.numberOfSegments; i++) {
[s setTitle:NSLocalizedString([s titleForSegmentAtIndex:i],nil) forSegmentAtIndex:i];
}
}
I was looking for the exactly answer given by Flax, marked as right, but I needed it in Swift. So I translated into it. Thanks Flax.
func replaceTextWithLocalizedTextInSubviewsForView(view: UIView) {
for v in view.subviews {
if v.subviews.count > 0 {
self.replaceTextWithLocalizedTextInSubviewsForView(v)
}
if (v.isKindOfClass(UILabel)) {
let myLabel: UILabel = v as! UILabel
myLabel.text = NSLocalizedString(myLabel.text!, comment: "Text to translate.")
myLabel.sizeToFit()
}
if (v.isKindOfClass(UIButton)) {
let myButton: UIButton = v as! UIButton
myButton.setTitle(NSLocalizedString((myButton.titleLabel?.text)!, comment: "Text to translate.") as String, forState: .Normal)
myButton.sizeToFit()
}
}
}
That works for Swift 2.1
I used a similar approach as Leszek Szary described for my views in Swift.
Using a Boolean value as opposed to the localization keys, I added an "On/Off" drop down menu that determines whether the initial text values should be localized or not. This allows for the Storyboard to be kept clean without any extra upkeep.
When a value is selected, a single Runtime Attribute is added to the view and is used as a condition from within it's setter.
Here is the code from my .swift file which extends UIButton, UILabel, UITabBarItem and UITextField, including the text field placeholder and button control states:
import UIKit
extension String {
public var localize: String {
return NSLocalizedString(self, comment: "")
}
}
extension UIButton {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) {
setTitle( title(for:.normal)?.localize, for:.normal)
setTitle( title(for:.highlighted)?.localize, for:.highlighted)
setTitle( title(for:.selected)?.localize, for:.selected)
setTitle( title(for:.disabled)?.localize, for:.disabled)
}}
}
}
extension UILabel {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) { text = text?.localize }}
}
}
extension UITabBarItem {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) { title = title?.localize }}
}
}
extension UITextField {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) {
placeholder = placeholder?.localize
text = text?.localize
}}
}
}
You could also use the new property to easily translate values that are set while your program is running like this:
let button = UIButton()
button.setTitle("Normal Text", for: .normal)
button.setTitle("Selected Text", for: .selected)
button.Localize = true
I came across this post and several others while trying to make xib localization easier for myself. I posted my method of including IBOutles for labels/buttons on this question, worked great for me, keeps all changes limited to the Localization.strings files.
https://stackoverflow.com/a/15485572/1449834
IMHO Xcode has one among the worst localization features available around...
I really don't like developing for Android but I must admit Android Studio has a better localization system.
That said, because I really cannot stand anymore to recreate Storyboard.strings after each mod (you know, Xcode won't update them for you...), this is how I do :
I have a couple of extensions to loop subviews (and subviews of subviews) and I deal with each of the main objects (labels, textfield, buttons...) by localizing their main properties (text, placeholde...) through a simple helper (AltoUtil.ls) which is a "short" version for NSLocalizedString.
Then I insert texts and placeholders with underscores (for example "_first_name", "_email_address") in my storyboard/xibs and I add those strings to each Localizable.strings file.
Now I just need to call the localize() function in viewDidLoad (or whereber I need it) so that I can have the whole view controller localized. For cells I just call the localize() inside the awakeFromNib() method for example.
I'm sure this is not the fastest method (due to subviews loop) but I don't get any slowdown compared to other methods I used to use and it's pretty productive.
import UIKit
extension UIView {
func localize()
{
for view in self.allSubviews()
{
if let label = view as? UILabel
{
label.text = AltoUtil.ls(label.text)
}
else if let textField = view as? UITextField
{
textField.text = AltoUtil.ls(textField.text)
textField.placeholder = AltoUtil.ls(textField.placeholder)
}
else if let button = view as? UIButton
{
button.setTitle(AltoUtil.ls(button.title(for: UIControl.State.normal)), for: UIControl.State.normal)
}
else if let searchBar = view as? UISearchBar
{
searchBar.placeholder = AltoUtil.ls(searchBar.placeholder)
}
}
}
func allSubviews() -> [UIView]
{
return subviews + subviews.flatMap { $0.allSubviews() }
}
}
The second extension is needed to localize view controllers title and tab bar items in view controllers. You can add any item you need to localize.
import UIKit
extension UIViewController {
func localize()
{
self.title = AltoUtil.ls(self.navigationItem.title)
self.tabBarItem?.title = AltoUtil.ls(self.tabBarItem?.title)
self.view.localize()
}
}

Resources