Swift: Convert object instance name to String for use as key - swift

Given var varName = "varValue", is there a way in Swift to convert the variable name to String at runtime? For example in this case I would get back "varName".
I already know about Mirror APIs for reflection in Swift. That allows me to iterate over the properties of a given class but I would like to apply this to self for any given class. I want to use this to generate String keys automatically for any given object (irrespective of which class it belongs to)
extension UIView {
var key: String {
return "" //TODO: Convert self to varName as String
}
}
// Usage
let customView = UIView()
customView.key // should be "customView"

Update:
The OP added this comment clarifying the requirements:
I need the 'key' to be different for 2 instances of UIView. I want that key to be the same every time for that particular instance i.e. the key shouldn't change if the app is restarted or the view instance is destroyed and recreated. I can use this key as key in caching. Another use case can be to use it as accessibilityIdentifier to help with UITesting.
In that case, I suggest to not even think about using ✨magic✨. Just explicitly give your view instances an identifier. You could also totally just reuse existing properties on UIView like tag or accessibilityIdentifier. If that's not enough or not convenient enough, subclass:
class IdentifiableView: UIView {
public private(set) var identifier: String
init(frame: CGRect, identifier: String) {
self.identifier = identifier
super.init(frame: frame)
self.accessibilityIdentifier = identifier
}
init() {
fatalError("Must use init(frame:identifier:)")
}
override init(frame: CGRect) {
fatalError("Must use init(frame:identifier:)")
}
required init?(coder aDecoder: NSCoder) {
fatalError("Must use init(frame:identifier:)")
}
}
// Usage
let firstView = IdentifiableView(frame: .zero, identifier: "First View")
firstView.identifier
firstView.identifier
let otherView = IdentifiableView(frame: .zero, identifier: "Second View")
otherView.identifier
otherView.identifier
If, according to your comment, you simply want "objects to return a unique key that does not change", you could simply use their memory address:
extension UIView {
var key: String {
return "\(Unmanaged.passUnretained(self).toOpaque())"
}
}
let firstView = UIView()
firstView.key // -> "0x00007fbc29d02f10"
firstView.key // -> "0x00007fbc29d02f10"
let otherView = UIView()
otherView.key // -> "0x00007fbc29d06920"
otherView.key // -> "0x00007fbc29d06920"
For each instance of UIView you create this will return a unique value that will not change.

I'm not sure what you are trying to accomplish here but I think you can convert variable name to string in Swift with #keyPath(propertyName) but it requires you to add #objc to your var.
For example
class MyViewController : UIViewController {
#objc var name: String = "John"
}
print(#keyPath(MyViewController.name))
prints name in the console.

Related

How to stop variable from going back to default value when read in different file

I have a DiscoveredSerialNumbers class that I want to access from various swift files:
class DiscoveredSerialNumbers {
var snConnect: String = ""
}
In my ViewController I change the value of snConnect based on the selection from a Picker View.
class ViewController: UIViewController, UIPickerViewDataSource,UIPickerViewDelegate {
#IBOutlet weak var SerialNumbers: UIPickerView!
var serialNums: [String] = [String]()
...
override func viewDidLoad() {
...
SerialNumbers.dataSource = self
SerialNumbers.delegate = self
}
...
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let global = DiscoveredSerialNumbers()
global.snConnect = serialNums[row]
print(serialNums[row])
print(global.snConnect)
}
}
When I print out the new value of snConnect set in the following line:
global.snConnect = serialNums[row]
Immediately afterward I get the new updated value of snConnect.
However, when I try to access the updated value of snConnect in a different swift file that controls a different ViewController in the following code:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
let global = DiscoveredSerialNumbers()
var sn = global.snConnect
...
}
The value of snConnect reverts back to the original value which is "".
How do I stop the value from reverting back to the initial value? I think it has something to do with me initializing the class DiscoveredSerialNumbers but I do not know how to access the value of snConnect in a different swift file otherwise.
Edit: Thanks to Don's comments, I am trying to have the snConnect value persist between instances of the application launching. I want to set the value of snConnect in the main app and access it when I launch an extension to the main app, in this case a custom keyboard extension.
Update: Question was a bit misleading you actually need to save the variable. I'm not sure if UserDefaults for app and keyboard extension are the same, you can try this.
class DiscoveredSerialNumbers {
static var main = DiscoveredSerialNumbers()
var snConnect: String {
get {
// Read from UserDefaults
return UserDefaults.standard.string(forKey: "snConnect") ?? ""
}
set {
// Save to UserDefaults
UserDefaults.standard.set(newValue, forKey: "snConnect")
}
}
init() {
print("New instance of DiscoveredSerialNumbers initialized.")
}
}
You can do this with a number of different ways,
however easiest one is creating a singleton of DiscoveredSerialNumbers() object, so your object and values can be used globally through it.
(although this method should be used with caution, it can cause a number of problems)
class DiscoveredSerialNumbers {
static var main = DiscoveredSerialNumbers()
var snConnect: String = ""
init() {
print("New instance of DiscoveredSerialNumbers initialized.")
}
}
now whenever you call DiscoveredSerialNumbers.main.snConnect old value will be kept and can be used/changed from anywhere.
Edit: Here's a sample Playground code for you to test out how singletons work
class Singleton
{
var someVariable = ""
static var main = Singleton()
}
class ClassA
{
init() {
Singleton.main.someVariable = "Hey I was changed in Class A"
}
}
class ClassB
{
init() {
print(Singleton.main.someVariable)
Singleton.main.someVariable = "And now I'm changed in class B"
}
}
let _ = ClassA()
let _ = ClassB()
print(Singleton.main.someVariable)
For an app extension to access data stored through it's container app, both the application and extension need to be part of the same app group. App groups are set in Signing & Capabilities section of Xcode for your project.
Once your app and extension are part of the same app group, you can use the following code to set the value of a global variable:
let defaults = UserDefaults(suiteName:"group.dataShare")
defaults?.set(serialNums[row], forKey: "snConnect")
Where group.dataShare is the name of your App group.
To retrieve the value, you can use the following code in your extension:
let defaults = UserDefaults(suiteName:"group.dataShareImada")
var sn = defaults?.string(forKey: "snConnect")

Initialize instance variables inside instance function

final class TestVC: UIViewController {
var usersFooter: Footer!
var groupsFooter: Footer!
override func viewDidLoad() {
super.viewDidLoad()
bind(footer: &usersFooter)
}
func bind(footer: inout Footer) {
footer = Footer(style: .autoFooter, height: 57) {
// doing something
}
}
}
Thats what Footer is:
final class Footer: RefreshView {
private var loader: MDCActivityIndicator!
override public init(style: Style, height: CGFloat, action: #escaping () -> Void) {
// initializing and doing something with loader
super.init(style: style, height: height, action: action)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
I get this:
Cannot pass immutable value of type 'Footer' as inout argument
How to pass TestVC instance's footers in it's function and be able to initialize them?
Why is footer that immutable (Declared as var)?
This occurs because
var someVar: Footer!
does not define a variable of type Footer but a variable of type Optional<Footer> that is implicitly unwrapped. The code:
var someFooter: Footer!
bind(footer: &someFooter)
is logically equivalent to
var someFooter: Footer?
guard let tempFooter = someFooter? else { fatalError() }
bind(footer: &tempFooter)
As you can see, tempFooter is a let, so it can't be passed as an inout variable and even if it could, the result would be thrown away.
You can fix this in one of three ways:
make the parameter to bind an optional, e.g. func bind(footer: inout Footer?) or use Martin's syntax to make it implicitly optional.
Force the unwrap yourself:
var unwrapped: Footer = someFooter
bind(footer: unwrapped)
someFooter = unwrapped
redesign the API. It seems the first thing you do in the bind function is overwrite the old footer with a newly initialised footer. So don't use an inout parameter, return the value you want i.e.
func bind() -> Footer
{
var theFooter = Footer(...) { ... }
// do stuff
return theFooter
}
someFooter = bind()
I think the last option is the best in this case.
Write bind method like this. It will resolve your error.
func bind(footer: inout Footer!) {
footer = Footer(style: .autoFooter, height: 57) {
// doing something
}
}
It seems like inout thinks Footer & Footer! are different. Either update method as above or change declaration as given below.
var userFooter: Footer
I don't know the exact reason but the error which we are getting is confusing.

Initialization of a UIViewController with default values consisted of singletons

Trying to refactor my code in order to avoid having to reference shared instances all over the place, rather I'd like to inject them via a custom initializer. My limited understanding of initilazation in Swift is preventing me to do so.
Here's my ViewController, minus the methods and boilerplate:
class LoginVC: UIViewController {
let dataManager: DataManager
let restManager: RestManager
let defaults: UserDefaults
init(defaults: UserDefaults = .standard, dataManager: DataManager = .sharedInstance, restManager: RestManager = .sharedInstance) {
self.defaults = defaults
self.dataManager = dataManager
self.restManager = restManager
super.init(nibName: nil, bundle: nil)
}
}
I've provided default values which are shared instances declared as a static constant in their respective singleton classes of DataManager and RestManager. Was planning to do this in each and every ViewController that has these singletons referenced in them. Anyways, my problem arises as the compiler complains that I must provide a required initializer init(coder: aDecoder).
No problem, but after implementing...
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
... the compiler once again complains, this time that
Blockquote Property 'self.defaults' not initialized at super.init call
Which I guess makes sense since the required init? is, as far as I know, a delegating init (could be that I'm totally mistaken here). I do not know how to pass the default params of my custom init to the super init of this initializer, or if that's even possible.
I have tried a workaround though by changing the constants to mutable variables and by making them implicitly unwrapped like so:
class LoginVC: UIViewController {
var dataManager: DataManager!
var restManager: RestManager!
var defaults: UserDefaults!
init(defaults: UserDefaults = .standard, dataManager: DataManager = .sharedInstance, restManager: RestManager = .sharedInstance) {
self.defaults = defaults
self.dataManager = dataManager
self.restManager = restManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
This workaround actually manages to compile and build, but the app crashes as soon as userDefaults which are in viewDidLoad are unwrapped because they have a nil value. All of them have nil values even though they are defaulted in the params of the custom initializer which leads me to believe that I've royally screwed up.
Any help/clarification would be greatly appreciated, thanks in advance.
Not sure that this is what you would like to do, but I would suggest you this solution
extension UIViewController {
var defaults: UserDefaults {
get {
return .standard
}
}
}
and so in each UIViewController you can do self.defaults
If you want some of your UIViewControllers to have another references, you can do this
//create protocol with all vars
protocol References {
var defaults: UserDefaults {get}
}
//create extension with default implementation
extension References {
var defaults: UserDefaults {
get {
return .standard
}
}
}
//add this protocol to all UIViewControllers
extension UIViewController: References {
}
//or create new class with references
class ReferenceViewController: UIViewController, References {
}
//override reference in some view controller if needed
class AnotherReferencesViewController: UIViewController { //or ReferenceViewController
var defaults: UserDefaults {
get {
//something new
return .standard
}
}
}

Using NSTreeController with NSOutlineView

I'm trying (unsuccessfully) to build a TreeController-controlled NSOutlineView. I've gone through a bunch of tutorials, but they all pre-load the data before starting anything, and this won't work for me.
I have a simple class for a device:
import Cocoa
class Device: NSObject {
let name : String
var children = [Service]()
var serviceNo = 1
var count = 0
init(name: String){
self.name = name
}
func addService(serviceName: String){
let serv = "\(serviceName) # \(serviceNo)"
children.append(Service(name: serv))
serviceNo += 1
count = children.count
}
func isLeaf() -> Bool {
return children.count < 1
}
}
I also have an even more simple class for the 'Service':
import Cocoa
class Service: NSObject {
let name: String
init(name: String){
self.name = name
}
}
Finally, I have the ViewController:
class ViewController: NSViewController {
var stepper = 0
dynamic var devices = [Device]()
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
}
#IBAction func addService(_ sender: Any) {
for i in 0..<devices.count {
devices[i].addService(serviceName: "New Service")
}
}
}
Obviously I have 2 buttons, one that adds a 'device' and one that adds a 'service' to each device.
What I can't make happen is any of this data show up in the NSOutlineView. I've set the TreeController's Object Controller Property to Mode: Class and Class: Device, and without setting the Children, Count, or Leaf properties I get (predictably):
2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder
If I then set the Children property to 'children' things go very bad:
2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. Key path: children
All I'm trying to do is set up the NSOutlineView to take input from the NSTreeController so that when a new 'Device' is added to the devices[] array, it shows up in the Outline View.
If anyone could point me in the right direction here I'd be most grateful.
Much gratitude to Warren for the hugely helpful work. I've got it (mostly) working. A couple of things that I also needed to do, in addition to Warren's suggestions:
Set the datastore for the Tree Controller
Bind the OutlineView to the TreeController
Bind the Column to the TreeController
Bind the TableView Cell to the Table Cell View (yes, really)
Once all that was done, I had to play around with the actual datastore a bit:
var name = "Bluetooth Devices Root"
var deviceStore = [Device]()
#IBOutlet var treeController: NSTreeController!
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
deviceStore.append(Device(name: "Bluetooth Devices"))
self.treeController.content = self
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
if(deviceStore[0].name == "Bluetooth Devices"){
deviceStore.remove(at: 0)
}
Turns out the Root cannot be child-less at the beginning, at least as far as I can tell. Once I add a child, I can delete the place-holder value and the tree seems to work (mostly) as I want. One other thing is that I have to reload the data and redisplay the outline whenever the data changes:
outlineView.reloadData()
outlineView.setNeedsDisplay()
Without that, nothing. I still don't have the data updating correctly (see comments below Warren's answer) but I'm almost there.
To state the obvious, a NSTreeController manages a tree of objects all of which need to answer the following three questions/requests.
Are you a leaf i.e do you have no children? = leafKeyPath
If you are not a leaf, how many children do you have ? = countKeyPath
Give me your children! = childrenKeyPath
Its simple to set these up in IB or programatically. A fairly standard set of properties is respectively.
isLeaf
childCount
children
But its totally arbitrary and can be any set of properties that answer those questions.
I normally set up a protocol named something like TreeNode and make all my objects conform to it.
#objc protocol TreeNode:class {
var isLeaf:Bool { get }
var childCount:Int { get }
var children:[TreeNode] { get }
}
For your Device object you answer 2 out 3 question with isLeaf and children but don't answer the childCount question.
Your Device's children are Service objects and they answer none of that which is some of the reason why you are getting the exceptions.
So to fix up your code a possible solution is ...
The Service object
class Service: NSObject, TreeNode {
let name: String
init(name: String){
self.name = name
}
var isLeaf:Bool {
return true
}
var childCount:Int {
return 0
}
var children:[TreeNode] {
return []
}
}
The Device object
class Device: NSObject, TreeNode {
let name : String
var serviceStore = [Service]()
init(name: String){
self.name = name
}
var isLeaf:Bool {
return serviceStore.isEmpty
}
var childCount:Int {
return serviceStore.count
}
var children:[TreeNode] {
return serviceStore
}
}
And a horrible thing to do from a MVC perspective but convenient for this answer. The root object.
class ViewController: NSViewController, TreeNode {
var deviceStore = [Device]()
var name = "Henry" //whatever you want to name your root
var isLeaf:Bool {
return deviceStore.isEmpty
}
var childCount:Int {
return deviceStore.count
}
var children:[TreeNode] {
return deviceStore
}
}
So all you need to do is set the content of your treeController. Lets assume you have an IBOutlet to it in your ViewController.
class ViewController: NSViewController, TreeNode {
#IBOutlet var treeController:NSTreeController!
#IBOutlet var outlineView:NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
treeController.content = self
}
Now each time you append a Device or add a Service just call reloadItem on the outlineView (that you also need an outlet to)
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
outlineView.reloadItem(self, reloadChildren: true)
}
Thats the basics and should get you started but the docs for NSOutlineView & NSTreeController have a lot more info.
EDIT
In addition to the stuff above you need to bind your outline view to your tree controller.
First ensure your Outline View is in view mode.
Next bind the table column to arrangedObjects on the tree controller.
Last bind the text cell to the relevant key path. In your case it's name. objectValue is the reference to your object in the cell.

Call external function using WatchKit force touch MenuItem

I need to implement a WatchKit force-touch MenuItem to call a saveWorkout() method that is located in a separate class that does not subclass WKInterfaceController.
I realize that every class needs at least one designated initializer. I am guessing this is the key?
Btw, my "saveSession() reached" print statement logs to the console when using the sim but not when I use a device. All other print statements log to the console even when using the device. A bit odd.
My attempts at initialization throw various errors such as:
1.fatal error: use of unimplemented initializer 'init()' for class 'DashboardController'
2.Missing argument for parameter 'context' in call
Dashboard.swift
class DashboardController: WKInterfaceController {
#IBOutlet var timerLabel: WKInterfaceTimer!
#IBOutlet weak var milesLabel: WKInterfaceLabel!
// var wSM: WorkoutSessionManager
//init(wSM: WorkoutSessionManager) {
// self.wSM = wSM
// super.init()
// }
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
addMenuItemWithItemIcon(.Accept, title: "Save", action: #selector(DashboardController.saveSession))
}
override func willActivate() {
super.willActivate()
print("Dashboard controller reached")
}
func saveSession() {
//wSM.saveWorkout()
print("saveSession() reached")
}
WorkoutSessionManager.swift
class WorkoutSessionContext {
let healthStore: HKHealthStore
let activityType: HKWorkoutActivityType
let locationType: HKWorkoutSessionLocationType
init(healthStore: HKHealthStore, activityType: HKWorkoutActivityType = .Other, locationType: HKWorkoutSessionLocationType = .Unknown) {
self.healthStore = healthStore
self.activityType = activityType
self.locationType = locationType
}
}
protocol WorkoutSessionManagerDelegate: class {
// ... protocol methods
}
class WorkoutSessionManager: NSObject, HKWorkoutSessionDelegate {
let healthStore: HKHealthStore
let workoutSession: HKWorkoutSession
init(context: WorkoutSessionContext) {
self.healthStore = context.healthStore
self.workoutSession = HKWorkoutSession(activityType: context.activityType, locationType: context.locationType)
self.currentActiveEnergyQuantity = HKQuantity(unit: self.energyUnit, doubleValue: 0.0)
self.currentDistanceQuantity = HKQuantity(unit: self.distanceUnit, doubleValue: 0.0)
super.init()
self.workoutSession.delegate = self
}
func saveWorkout() {
guard let startDate = self.workoutStartDate, endDate = self.workoutEndDate else {return}
// ...code...
The fatal error is (or was) caused by this line:
let wSM = WorkoutSessionManager()
That line creates a new instance of WorkoutSessionManager and calls init() on it.
Swift provides a default initializer called init() for any structure or class that provides default values for all of its properties and does not provide at least one initializer itself. But WorkoutSessionManager does not provide default values for the healthStore and workoutSession properties (and those properties are not optionals), and it provides its own initializer named init(context:), so it has no default initializer.
You need to either create your instance of WorkoutSessionManager using the designated initializer init(context:) (passing an appropriate instance of WorkoutSessionContext) or provide a default initializer for WorkoutSessionManager named init().
The precise manner in which you should do the former depends on the implementation of the rest of your app and the presentation of your DashboardController. I assume you are trying to recreate the "Fit" app shown in WWDC 2015 Session 203.
In that demonstration, the initial controller is an instance of ActivityInterfaceController, and that controller is responsible for presenting the next interface (via segues created in the storyboard). You can see the following code in the ActivityInterfaceController class:
override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
let activityType: HKWorkoutActivityType
switch segueIdentifier {
case "Running":
activityType = .Running
case "Walking":
activityType = .Walking
case "Cycling":
activityType = .Cycling
default:
activityType = .Other
}
return WorkoutSessionContext(healthStore: self.healthStore, activityType: activityType)
}
The function above creates and returns a new instance of WorkoutSessionContext using an instance of HKHealthStore held by the initial controller. The context returned by that function is passed to the destination interface controller for the relevant segue through awakeWithContext.
For transitions in code, you can pass a context instance using equivalent functions such as pushControllerWithName(context:) which also lead to awakeWithContext.
If your initial controller is similar to the above, you can access the passed context in awakeWithContext in your DashboardController class and use it to configure a new instance of WorkoutSessionManager:
class DashboardController: WKInterfaceController
{
// ...
var wSM: WorkoutSessionManager?
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
if context is WorkoutSessionContext {
wSM = WorkoutSessionManager(context: context as! WorkoutSessionContext)
}
addMenuItemWithItemIcon(.Accept, title: "Save", action: #selector(DashboardController.saveSession))
}
// ...
}
Creating an instance of WorkoutSessionManager in that way avoids calling the (non-existent) init() initializer and permits reuse of the HKHealthStore instance. Whether that approach is open to you depends on the rest of your code and the way you are presenting your DashboardController.
Note that you should avoid creating multiple instances of WorkoutSessionManager. Use a singleton to provide a single instance of WorkoutSessionManager that is shared across your extension.