Objc-C to Swift: How to create a property in Swift that guarantees it's of a certain type when callers use it? - objective-c

TL;DR: This is about migrating an Objective-C pattern to Swift. It might be best to first look at the Objective-C interface below to better understand what I'm trying to achieve.
I am just starting to adapt a rather large codebase from Objective-C to Swift. There were some design patterns in the legacy codebase that were put in place to try and provide some type-safety.
These patterns seem really out of place in Swift but I'm not sure what the proper "Swift Way" of doing this is. Using Generics feels like the way to go about it, but I'm unclear on how best to proceed.
The goal is to create a struct that has a property that can hold "almost anything". Callers are expecting the property to be of a certain type when used and an error or exception should be thrown if there is a type-mismatch. (i.e.: The caller expected the argument to be an integer but in reality a string was store.)
struct Command<T> {
let directive: Directive
let argument: T
}
let command = Command(directive: .draw, argument: NSZeroRect)
let command2 = Command(directive: .toggle, argument: true)
// Somewhere else in the code...
//
// How do I pass in a Command<> here?
// This generates an error because Command<Bool> cannot be converted to Command<Any>
//
func processCommand(_ command:Command<Any>) {
switch command.directive {
case .draw:
// How do I ensure that command.argument is indeed an NSRect?
case .toggle:
// How do I ensure that command.argument is indeed a boolean?
}
}
The Objective-C interface looks something like this. Note that argument can be many different types. Ranging from primitives (Integers, Booleans, Doubles, etc...) to anything that can be stored in NSValue or that supports NSCoding.
There are multiple property accessors for each type where it makes sense.
#interface FLCommand : NSObject
#property(assign, readonly) FLDirective directive;
#property(strong, readonly) id argument;
#property(strong, readonly) BOOL argumentAsBoolean;
#property(strong, readonly) NSRect argumentAsRect;
- (instancetype)initWithDirective:(FLDirective)directive booleanArgument:(BOOL)value;
- (instancetype)initWithDirective:(FLDirective)directive rectArgument:(NSRect)rect;
- (instancetype)initWithDirective:(FLDirective)directive argument:(id)arg;
#end
#implementation FLCommand
- (instancetype)initWithDirective:(FLDirective)directive
booleanValue:(BOOL)value {
// Convert boolean to object.
return [self initWithDirective:directive
argument:#(value)];
}
- (instancetype)initWithDirective:(FLDirective)directive
rectArgument:(NSRect)rect {
// Convert NSRect to object.
return [self initWithDirective:directive
argument:[NSValue valueWithRect:rect]];
}
- (BOOL)argumentAsBoolean {
NSAssert([_argument isKindOfClass:NSNumber.class], #"Expected argument to be an NSNumber.");
return [self.argument boolValue];
}
- (NSRect)argumentAsRect {
NSAssert([_argument isKindOfClass:NSValue.class], #"Expected command argument to be an NSValue.");
return [(NSValue *)self.argument rectValue];
}
#end
// Somewhere else in the code the commands are acted upon. Using the
// asserts and type-specific property accessors offers a poor-man's
// way of doing type safety to ensure the the command's argument is
// of the expected type.
- (void)processCommand:(FLCommand *)command {
switch (command.directive) {
case FLDirectiveToggleSomething:
// The assert will fire if the argument is not a boolean.
[self toggleSomething:command.argumentAsBoolean];
break;
case FLDirectiveDrawSomething:
[self drawSomethingInFrame:command.argumentAsRect];
break;
}
}
}
Using an equivalent pattern in Swift seems very un-Swift like to me. Is there a better way to go about this using Generics?
Swift 5 and macOS 10.15+ solutions are OK.

Have you considered using enumerations with associated values (often referred to as complex enums)
enum Directive {
case draw(NSRect)
case toggle(Bool)
}
struct Command {
let directive: Directive
}
let command = Command(directive: .draw(.zero))
let command2 = Command(directive: .toggle(true))
func processCommand(_ command: Command) {
switch command.directive {
case .draw(let rect):
// do something with rect
case .toggle(let value):
// do something with the value
}
}
(And you could actually skip the Command struct entirely in the above)
Or an alternative solution is to use a protocol with an associated type:
protocol Command {
associatedtype AssociatedType
var argument: AssociatedType { get }
init(_ argument: AssociatedType)
func process()
}
struct DrawCommand: Command {
typealias AssociatedType = NSRect
let argument: AssociatedType
init(_ argument: AssociatedType) {
self.argument = argument
}
func process() {
print("draw something with \(argument)")
}
}
struct ToggleCommand: Command {
typealias AssociatedType = Bool
let argument: AssociatedType
init(_ argument: AssociatedType) {
self.argument = argument
}
func process() {
print("toggle something with \(argument)")
}
}
let command = DrawCommand(.zero)
let command2 = ToggleCommand(true)
command.process()
command2.process()
This have a bit more boilerplate/overload, but provides a better separation of concerns and will be more flexible for you introducing more commands in the future without having to update enum/switches multiple places in the code.

Related

Swift #objc protocol cannot be used as a type conforming to protocol 'Equatable' because 'Equatable' has static requirements

I am currently writing a reusable UI component in Swift that's supposed to be consumed from both Obj-C/ Swift worlds (it's a mixed project). I defined a #objc protocol without any associated types (since those are not allowed for #objc protocols). In one of the methods in the component, I need to store the protocol as a type and need to find the index of a particular entry, somewhat similar to the following-
func select<T: Itemable>(_ item: T) {
guard let itemIndex = items.index(of: item) else {
return
}
//more stuf
}
where items is an array of Itemable (protocol) type.
However, I get the error saying I can not use it as a type conforming to Equatable since equatable has static requirements.
Itemable is defined as following-
#objc protocol Itemable {
//methods and properties
}
Also, not sure how to make it conform to equatable. Apparently, the following helps but not sure why-
func ==<T: <Itemable>>(lhs: T, rhs: T) -> Bool {
return lhs.aProperty == rhs.aProperty
}
Seems to me like it might require type erasing, but not sure how to go about doing that.
Here's an abridged version of the protocol, showing all different types of methods and properties present- it does not really have anything static or associated type.
#objc protocol Itemable {
typealias voidBlock = () -> Void
var color: UIColor { get }
var screenParameters: [String : String] { get }
var screenView: String { get }
var iconImage: UIImage? { get }
#objc var accessibilityLabel: String? { get }
}
extension Array where Element: Itemable {
func index(of element: Element) -> Int? {
index(where: { $0.screenView == element.screenView })
}
}
You cannot make an #objc type conform to Equatable. Equatable has a Self requirement. Objective-C protocols cannot express a Self requirement.
Your == function is usable, but it doesn't cause the type to conform to Equatable. It just means you can evaluate item == item. You cannot, however, call items.contain(item) since Itemable doesn't conform to Equatable. What you can do is call items.contains{$0 == item} since that just requires the == function, not Equatable. And of course you could implement a custom .contains method for [Itemable] if you wanted one. But it still wouldn't be Equatable.
For your example, I believe you want to get rid of == entirely, and use this:
guard let itemIndex = items.index(where: { $0.aProperty == item.aProperty }) else {
If you do this a lot, you can of course add an extension on [Itemable] (untested):
extension Array where Element: Itemable {
func index(of element: Element) -> Int? {
firstIndex(where: { $0.aProperty == element.aProperty })
}
}
Then your original code would be fine.
Somewhat unrelated to your question: It's possible this is simplified code, but be very careful of this kind of implementation of ==. First, == should always test all visible properties. If, for example, aProperty is just the ID, then that is a dangerous way to implement ==. When two things are equal (in both ObjC and Swift), they are expected to be interchangeable in all contexts. If you ever care which one you have, they're not really "equal."
Also, when two things are equal, they should have the same hash (and if they're Equatable, they must have the same hash).
See the docs on Conforming to the Equatable Protocol for the rules. While == doesn't technically imply Equatable, it is confusing to use it if you don't mean Equatable.

How do I access private instance variables in Obj-C or Swift?

I'm trying to get access to MKMapSnapshotter's private instance variables _lodpiSnapshotCreator and _hidpiSnapshotCreator in Swift on macOS.
Thanks to class-dump, I know they're there (see here):
#interface MKMapSnapshotter : NSObject
{
[...]
VKMapSnapshotCreator *_lodpiSnapshotCreator;
VKMapSnapshotCreator *_hidpiSnapshotCreator;
}
but no matter what I do, I can't get ahold of them.
This is how I checked whether I could access them or not:
let snapshotter = MKMapSnapshotter(options: snapshotOptions)
let varNames = ["_lodpiSnapshotCreator", "_hidpiSnapshotCreator"]
for varName in varNames {
if let testIVar = class_getInstanceVariable(MKMapSnapshotter.self, varName) {
if let test = object_getIvar(snapshotter, testIVar) as? VKMapSnapshotCreator {
print(test)
} else {
print("Got ivar, but \(varName) is still nil (getInstanceVariable)")
}
} else {
print("\(varName) is nil (getInstanceVariable)")
}
}
Curiously, class_getInstanceVariable doesn't return nil, but object_getIvar does.
Got ivar, but _lodpiSnapshotCreator is still nil (getInstanceVariable)
Got ivar, but _hidpiSnapshotCreator is still nil (getInstanceVariable)
I'm at my wit's end here. All I can find via Google is people recommending the use of class_getInstanceVariable (which I already use) and key-value-coding (which doesn't work at all).
This must have been done before. Can anyone help me out?
Edit: So far, I have tried this:
#interface MKMapSnapshotter() {
#public VKMapSnapshotCreator *_lodpiSnapshotCreator;
#public VKMapSnapshotCreator *_hidpiSnapshotCreator;
}
#end
That compiles successfully, but when trying to use it, Swift keeps insisting that the members _lodpiSnapshotCreator and _hidpiSnapshotCreator don't exist.
Since we don't have or control the source code we can't change the variables to properties. Just tried this works for your case:
extension MKMapSnapshotter {
func getPrivateVariable() -> String? {
return value(forKey: "_lodpiSnapshotCreator") as? String
}
open override func value(forUndefinedKey key: String) -> Any? {
if key == "_lodpiSnapshotCreator" {
return nil
}
return super.value(forUndefinedKey: key)
}
}
You can find more about this here.
If this does not work then I believe that there is no way to access Objective-C instance variables from Swift. Only Objective-C properties get mapped to Swift properties.
Hope this helps!!

Creating an enum in Swift, exportable to ObjC, based on another enum: String

I have an enum Foo: String in Swift (therefore not exportable to Objective-C) and I'm trying to create another enum FooObjc in Swift to kinda "wrap" the existent one so that it is 1) available to use in Objective-C and 2) convertable back and forth (Foo <=> FooObjc). The original Foo enum is part of a framework that I don't want to modify. Of course, it's very easy to do what I want if I use a class instead, like this:
#objc public class FooObjc: NSObject {
public private(set) var foo: Foo
#objc public var string: String {
return foo.rawValue
}
#objc public init?(string: NSString) {
guard let foo = Foo(rawValue: string as String) else {
return nil
}
self.foo = foo
}
internal init(foo: Foo) {
self.foo = foo
}
}
(PS: not able to inherit from NSString because the Swift compiler still doesn't accept creating initializers for that class)
Ok, but I definitely don't like this approach because then my Objective-C code will be string-typed. I really want to use an enum instead because after all, that's what it is. This is the least bad working version I could get:
#objc public enum FooObjc: Int, RawRepresentable {
case undefined = -1
case bar
case baz
// omitting many more cases
internal init?(_ foo: Foo?) {
if let foo = foo {
self = fooMapping.filter { $1 == foo }.map { $0.key }.first!
} else {
self = .undefined
}
}
// MARK: - RawRepresentable
public typealias RawValue = String
public init?(rawValue: RawValue) {
let filter = fooMapping.filter { $1?.rawValue == rawValue }
guard filter.count > 0 else {
return nil
}
self = filter.map { $0.key }.first!
}
public var rawValue: RawValue {
switch (self) {
case .undefined: return "undefined"
case .bar: return Foo.bar.rawValue
case .baz: return Foo.baz.rawValue
// omitting many more cases
}
}
}
private let fooMapping: [FooObjc: Foo?] = [
.undefined : nil,
.bar : .bar,
.baz : .baz
// omitting many more cases
]
Notice that:
the fooMapping is useful to avoid one switch-case for each initializer
this undefined case was necessary because in Swift you can have optional enum properties, in Objective-C you can't, so this case will directly map to a Foo? which value is nil.
What worries me here is that I had to write the same cases from the original Foo three times... I'm completely satisfied if I repeat it only twice, but I wasn't able to use the fooMapping in the rawValue property because then I get a cycle between these two.
Note: I'm not sure if this is relevant to the question, but in the original enum, some of the cases have special String attributions, e.g. we have simply case bar but we also have case baz = "something".
So, the question is: does anyone have suggestions to improve this approach or even suggest something completely new that would avoid so much code repetition?
Thank you very much!
What worries me here is that I had to write the same cases from the original Foo three times
Consider an array ["bar", "baz" ...]. By looking at the index of a string in this array and making the necessary adjustments, you can translate from a string to an integer (and thence, via raw value, to a case). By indexing into the array and making the necessary adjustments, you can translate from an integer to a string (and thence, via raw value, to a case). So you will only have to write out the string values once. This eliminates two of your repetitions.
You will still have to write out the case names in the enum declaration, because there's no other way to tell Objective-C the names of the cases. The way to eliminate that repetition, obviously, is to be willing to change the implementation of Foo itself so that it becomes Objective-C-compatible. But you have stated up front that you refuse to do that, so now you must pay the price.

Objective-C Block in Swift - Variable Missing?

Here's a block type that I am defining in objective-c
typedef void (^arrayBlock)(NSArray *blockArray);
I have an objective-c class with a method that uses this as a return block
-(void)loadTimesWithSuccessBlock:(arrayBlock)successBlock;
When I try to use this method in Swift, this is what autocomplete gives me.
let timeClockLibrarian = HMFTimeClockLibrarian()
timeClockLibrarian.loadTimesWithSuccessBlock { ([AnyObject]!) -> Void in
//Where is blockArray?
}
I'm assuming that [AnyObject]! is supposed to be the NSArray. But I don't see how I'm supposed to get access to that variable?
If I were to use this method in Objective-C I get a result like this:
[timeClockLibrarian loadTimesWithSuccessBlock:^(NSArray *blockArray) {
//I can use the blockArray here :)
}];
[AnyObject]! is indeed only the type of the variable; autocomplete didn't name it. You just need to do something like (blockArray: [AnyObject]!).
let timeClockLibrarian = HMFTimeClockLibrarian()
timeClockLibrarian.loadTimesWithSuccessBlock { (blockArray: [AnyObject]!) -> Void in
// your code here
}
Write like this:
let timeClockLibrarian = HMFTimeClockLibrarian()
timeClockLibrarian.loadTimesWithSuccessBlock { blockArray in
doSomething(blockArray)
}
If you want to refer to weak self use this:
let timeClockLibrarian = HMFTimeClockLibrarian()
timeClockLibrarian.loadTimesWithSuccessBlock { [weak self] blockArray in
self?.doSomething(blockArray)
}
You may also want to get rid of implicit unwrapping. If so, specify nullability in Obj-C code:
typedef void (^arrayBlock)(nullable NSArray *blockArray);

Error in Swift class: Property not initialized at super.init call

I have two classes, Shape and Square
class Shape {
var numberOfSides = 0
var name: String
init(name:String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
class Square: Shape {
var sideLength: Double
init(sideLength:Double, name:String) {
super.init(name:name) // Error here
self.sideLength = sideLength
numberOfSides = 4
}
func area () -> Double {
return sideLength * sideLength
}
}
With the implementation above I get the error:
property 'self.sideLength' not initialized at super.init call
super.init(name:name)
Why do I have to set self.sideLength before calling super.init?
Quote from The Swift Programming Language, which answers your question:
“Swift’s compiler performs four helpful safety-checks to make sure
that two-phase initialization is completed without error:”
Safety check 1 “A designated initializer must ensure that all of the
“properties introduced by its class are initialized before it
delegates up to a superclass initializer.”
Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks.
https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11
Swift has a very clear, specific sequence of operations that are done in initializers. Let's start with some basic examples and work our way up to a general case.
Let's take an object A. We'll define it as follows.
class A {
var x: Int
init(x: Int) {
self.x = x
}
}
Notice that A does not have a superclass, so it cannot call a super.init() function as it does not exist.
OK, so now let's subclass A with a new class named B.
class B: A {
var y: Int
init(x: Int, y: Int) {
self.y = y
super.init(x: x)
}
}
This is a departure from Objective-C where [super init] would typically be called first before anything else. Not so in Swift. You are responsible for ensuring that your instance variables are in a consistent state before you do anything else, including calling methods (which includes your superclass' initializer).
From the docs
Safety check 1
A designated initializer must ensure that all of the properties
introduced by its class are initialized before it delegates up to a
superclass initializer.
Why do we need a safety check like this?
To answer this lets go though the initialization process in swift.
Two-Phase Initialization
Class initialization in Swift is a two-phase process. In the first
phase, each stored property is assigned an initial value by the class
that introduced it. Once the initial state for every stored property
has been determined, the second phase begins, and each class is given
the opportunity to customize its stored properties further before the
new instance is considered ready for use.
The use of a two-phase initialization process makes initialization
safe, while still giving complete flexibility to each class in a class
hierarchy. Two-phase initialization prevents property values from
being accessed before they are initialized, and prevents property
values from being set to a different value by another initializer
unexpectedly.
So, to make sure the two step initialization process is done as defined above, there are four safety checks, one of them is,
Safety check 1
A designated initializer must ensure that all of the properties
introduced by its class are initialized before it delegates up to a
superclass initializer.
Now, the two phase initialization never talks about order, but this safety check, introduces super.init to be ordered, after the initialization of all the properties.
Safety check 1 might seem irrelevant as,
Two-phase initialization prevents property values from being accessed before they are initialized can be satisfied, without this safety check 1.
Like in this sample
class Shape {
var name: String
var sides : Int
init(sides:Int, named: String) {
self.sides = sides
self.name = named
}
}
class Triangle: Shape {
var hypotenuse: Int
init(hypotenuse:Int) {
super.init(sides: 3, named: "Triangle")
self.hypotenuse = hypotenuse
}
}
Triangle.init has initialized, every property before being used. So Safety check 1 seems irrelevant,
But then there could be another scenario, a little bit complex,
class Shape {
var name: String
var sides : Int
init(sides:Int, named: String) {
self.sides = sides
self.name = named
printShapeDescription()
}
func printShapeDescription() {
print("Shape Name :\(self.name)")
print("Sides :\(self.sides)")
}
}
class Triangle: Shape {
var hypotenuse: Int
init(hypotenuse:Int) {
self.hypotenuse = hypotenuse
super.init(sides: 3, named: "Triangle")
}
override func printShapeDescription() {
super.printShapeDescription()
print("Hypotenuse :\(self.hypotenuse)")
}
}
let triangle = Triangle(hypotenuse: 12)
Output :
Shape Name :Triangle
Sides :3
Hypotenuse :12
Here if we had called the super.init before setting the hypotenuse, the super.init call would then have called the printShapeDescription() and since that has been overridden it would first fallback to Triangle class implementation of printShapeDescription(). The printShapeDescription() of Triangle class access the hypotenuse a non optional property that still has not been initialised. And this is not allowed as Two-phase initialization prevents property values from being accessed before they are initialized
So make sure the Two phase initialization is done as defined, there needs to be a specific order of calling super.init, and that is, after initializing all the properties introduced by self class, thus we need a Safety check 1
The "super.init()" should be called after you initialize all your instance variables.
In Apple's "Intermediate Swift" video (you can find it in Apple Developer video resource page https://developer.apple.com/videos/wwdc/2014/), at about 28:40, it is explicit said that all initializers in super class must be called AFTER you initialize your instance variables.
In Objective-C, it was the reverse. In Swift, since all properties need to be initialized before it's used, we need to initialize properties first. This is meant to prevent a call to overrided function from super class's "init()" method, without initializing properties first.
So the implementation of "Square" should be:
class Square: Shape {
var sideLength: Double
init(sideLength:Double, name:String) {
self.sideLength = sideLength
numberOfSides = 4
super.init(name:name) // Correct position for "super.init()"
}
func area () -> Double {
return sideLength * sideLength
}
}
Sorry for ugly formatting.
Just put a question character after declaration and everything will be ok.
A question tells the compiler that the value is optional.
class Square: Shape {
var sideLength: Double? // <=== like this ..
init(sideLength:Double, name:String) {
super.init(name:name) // Error here
self.sideLength = sideLength
numberOfSides = 4
}
func area () -> Double {
return sideLength * sideLength
}
}
Edit1:
There is a better way to skip this error. According to jmaschad's comment there is no reason to use optional in your case cause optionals are not comfortable in use and You always have to check if optional is not nil before accessing it. So all you have to do is to initialize member after declaration:
class Square: Shape {
var sideLength: Double=Double()
init(sideLength:Double, name:String) {
super.init(name:name)
self.sideLength = sideLength
numberOfSides = 4
}
func area () -> Double {
return sideLength * sideLength
}
}
Edit2:
After two minuses got on this answer I found even better way. If you want class member to be initialized in your constructor you must assign initial value to it inside contructor and before super.init() call. Like this:
class Square: Shape {
var sideLength: Double
init(sideLength:Double, name:String) {
self.sideLength = sideLength // <= before super.init call..
super.init(name:name)
numberOfSides = 4
}
func area () -> Double {
return sideLength * sideLength
}
}
Good luck in learning Swift.
swift enforces you to initialise every member var before it is ever/might ever be used. Since it can't be sure what happens when it is supers turn, it errors out: better safe than sorry
Edward,
You can modify the code in your example like this:
var playerShip:PlayerShip!
var deltaPoint = CGPointZero
init(size: CGSize)
{
super.init(size: size)
playerLayerNode.addChild(playerShip)
}
This is using an implicitly unwrapped optional.
In documentation we can read:
"As with optionals, if you don’t provide an initial value when you
declare an implicitly unwrapped optional variable or property, it’s
value automatically defaults to nil."
Swift will not allow you to initialise super class with out initialising the properties, reverse of Obj C. So you have to initialise all properties before calling "super.init".
Please go to http://blog.scottlogic.com/2014/11/20/swift-initialisation.html.
It gives a nice explanation to your problem.
Add nil to the end of the declaration.
// Must be nil or swift complains
var someProtocol:SomeProtocol? = nil
// Init the view
override init(frame: CGRect)
super.init(frame: frame)
...
This worked for my case, but may not work for yours
its should be this:
init(sideLength:Double, name:String) {
self.sideLength = sideLength
super.init(name:name)
numberOfSides = 4
}
look at this link:
https://swiftgg.gitbook.io/swift/swift-jiao-cheng/14_initialization#two-phase-initialization
You are just initing in the wrong order.
class Shape2 {
var numberOfSides = 0
var name: String
init(name:String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
class Square2: Shape2 {
var sideLength: Double
init(sideLength:Double, name:String) {
self.sideLength = sideLength
super.init(name:name) // It should be behind "self.sideLength = sideLength"
numberOfSides = 4
}
func area () -> Double {
return sideLength * sideLength
}
}
#Janos if you make the property optional, you don't have to initialise it in init. –
This worked for me.