Asked  6 Months ago    Answers:  5   Viewed   28 times

I'm trying to convert my project's source code from Swift 3 to Swift 4. One warning Xcode is giving me is about my selectors.

For instance, I add a target to a button using a regular selector like this:

button.addTarget(self, action: #selector(self.myAction), for: .touchUpInside)

This is the warning it shows:

Argument of '#selector' refers to instance method 'myAction()' in 'ViewController' that depends on '@objc' attribute inference deprecated in Swift 4

Add '@objc' to expose this instance method to Objective-C

Now, hitting Fix on the error message does this to my function:

// before
func myAction() { /* ... */ }

// after
@objc func myAction() { /* ... */ }

I don't really want to rename all of my functions to include the @objc mark and I'm assuming that's not necessary.

How do I rewrite the selector to deal with the deprecation?


Related question:

  • The use of Swift 3 @objc inference in Swift 4 mode is deprecated?

 Answers

85

The fix-it is correct – there's nothing about the selector you can change in order to make the method it refers to exposed to Objective-C.

The whole reason for this warning in the first place is the result of SE-0160. Prior to Swift 4, internal or higher Objective-C compatible members of NSObject inheriting classes were inferred to be @objc and therefore exposed to Objective-C, therefore allowing them to be called using selectors (as the Obj-C runtime is required in order to lookup the method implementation for a given selector).

However in Swift 4, this is no longer the case. Only very specific declarations are now inferred to be @objc, for example, overrides of @objc methods, implementations of @objc protocol requirements and declarations with attributes that imply @objc, such as @IBOutlet.

The motivation behind this, as detailed in the above linked proposal, is firstly to prevent method overloads in NSObject inheriting classes from colliding with each other due to having identical selectors. Secondly, it helps reduce the binary size by not having to generate thunks for members that don't need to be exposed to Obj-C, and thirdly improves the speed of dynamic linking.

If you want to expose a member to Obj-C, you need to mark it as @objc, for example:

class ViewController: UIViewController {

    @IBOutlet weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        button.addTarget(self, action: #selector(foo), for: .touchUpInside)
    }

    @objc func foo() {
       // ... 
    }
}

(the migrator should do this automatically for you with selectors when running with the "minimise inference" option selected)

To expose a group of members to Obj-C, you can use an @objc extension:

@objc extension ViewController {

    // both exposed to Obj-C
    func foo() {}
    func bar() {}
}

This will expose all the members defined in it to Obj-C, and give an error on any members that cannot be exposed to Obj-C (unless explicitly marked as @nonobjc).

If you have a class where you need all Obj-C compatible members to be exposed to Obj-C, you can mark the class as @objcMembers:

@objcMembers
class ViewController: UIViewController {
   // ...
}

Now, all members that can be inferred to be @objc will be. However, I wouldn't advise doing this unless you really need all members exposed to Obj-C, given the above mentioned downsides of having members unnecessarily exposed.

Tuesday, June 1, 2021
 
jenny
answered 6 Months ago
85

If you need to detect if a device is iPhoneX don't use bounds, it depends on the orientation of the device. So if the user opens your app in portrait mode it will fail. You can use Device property nativeBounds which doesn't change on rotation.

In iOS 8 and later, a screen’s bounds property takes the interface orientation of the screen into account. This behavior means that the bounds for a device in a portrait orientation may not be the same as the bounds for the device in a landscape orientation. Apps that rely on the screen dimensions can use the object in the fixedCoordinateSpace property as a fixed point of reference for any calculations they must make. (Prior to iOS 8, a screen’s bounds rectangle always reflected the screen dimensions relative to a portrait-up orientation. Rotating the device to a landscape or upside-down orientation did not change the bounds.)

extension UIDevice {
    var iPhoneX: Bool {
        return UIScreen.main.nativeBounds.height == 2436
    }
}

usage

if UIDevice.current.iPhoneX { 
    print("This device is a iPhoneX")
}
Friday, July 30, 2021
 
KHM
answered 4 Months ago
KHM
62

UPDATE 2:

This causes builds to be canceled! Have a look at S1LENT WARRIOR's answer below, it seems to be working better.

UPDATE 1:

In the latest version of Xcode (Version 11.1) you can do the build number auto increment fairly easily.

Here are the steps:

  1. Go to your target's Build Settings
  2. Search for Versioning System
  3. Set it's value to Apple Generic
  4. Go to your target's Build Phases
  5. Add a new Run Script
  6. Add the following line agvtool next-version -all

Do this for all your targets and their build numbers will all be synced and updated every time you run any of the targets.

Got this answer from here: https://stackoverflow.com/a/58237340/1432355

P.S.: As said in a comment below

Using avgtool in a Run Script Phase causes the build to get cancelled

ORIGINAL:

You didn't do anything wrong I think.

If you go to your info.plist you will see that the build number has been replaced by $(CURRENT_PROJECT_VERSION) (you can find the variable in the Build Settings tab).

I am guessing you are using a script that increments build number automatically and that is causing the issue (I have the same thing on my project right now).

If you remove that script your app should build without this error.

I haven't found a solution yet on how to make the script work with this new $(CURRENT_PROJECT_VERSION) variable. (I will update this answer when I have found the solution)

Friday, August 13, 2021
 
Riyazul Aboobucker
answered 4 Months ago
61

As for your initial code, here's what it should look like:

class myArrayController: NSArrayController {
    private var mySub: Any? = nil

    required init?(coder: NSCoder) {
        super.init(coder: coder)

        self.mySub = self.observe(.content, options: [.new]) { object, change in
            debugPrint("Observed a change to", object.content)
        }
    }
}

The observe(...) function returns a transient observer whose lifetime indicates how long you'll receive notifications for. If the returned observer is deinit'd, you will no longer receive notifications. In your case, you never retained the object so it died right after the method scope.

In addition, to manually stop observing, just set mySub to nil, which implicitly deinits the old observer object.

Wednesday, September 22, 2021
 
kamikaze_pilot
answered 2 Months ago
19

Use init(from decoder: Decoder) to set the default values in your model.

struct LoginModal: Codable {

    let cashierType: Int
    let status: Int

    enum CodingKeys: String, CodingKey {
        case cashierType = "cashierType"
        case status = "status"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.cashierType = try container.decodeIfPresent(Int.self, forKey: .cashierType) ?? 0
        self.status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0
    }
}

Data Reading:

do {
        let data = //JSON Data from API
        let jsonData = try JSONDecoder().decode(LoginModal.self, from: data)
        print("(jsonData.status) (jsonData.cashierType)")
    } catch let error {
        print(error.localizedDescription)
    }
Monday, November 8, 2021
 
Kyle Vassella
answered 3 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share