I do some contract work for a company I sold an app to awhile ago, and the codebase is mixed Objective-C/Swift. Anything new I do in Swift, but there's a few rare new view controllers and the like where Objective-C is used because it's just easier/faster to copy/paste infrastructure.
Some crypto code is also still Objective-C due to easier linking, but that'll probably change soon enough.
I don't consider ObjC to be a bad language in the slightest; in fact, I'll offer the (probably contrarian) opinion that it's one of the greatest languages of the past few decades.
I've never used it, but have seen many code snippets in API documentations, and it just looks so...unnecessarily obfuscated and verbose, and with a syntax so wholly unique to Obj-C that my brain can't make sense of it like I would looking at, say, Java code from a C++ background.
- As malleable as JavaScript when you need it to be
- As typed as your favorite language when you need it to be
- Able to drop to much more arcane levels if you need to for certain performance-intensive places, without requiring some crazy setup (e.g, C/C++ are right there if you need them)
- ARC is IMO one of the best approaches to memory management out there
- The verbosity can be annoying at first, but it forces you to think hard about everything, and when I come back to Objective-C code years later, I've no issue remembering what the hell I was doing there
- People complain about brackets, but they just... don't matter. It's a syntax. You either deal with it or don't - you don't see me writing Lisp because I find the syntax annoying, which is fine.
- Message passing in ObjC is so optimized that it probably can't get much faster, and anyone acting like it's slow has a potentially skewed understanding of this
Swift is nicer for me in only two distinct ways:
- No more header files, because man was that annoying
- The stricter nil handling is overall better, if not a mental shift from some ObjC counterparts
> - Message passing in ObjC is so optimized that it probably can't get much faster, and anyone acting like it's slow has a potentially skewed understanding of this
The problem with Objc message passing is not the absolute execution time involved in passing a message.
The problem is that the dynamism involved completely disables the compiler from performing any code-inlining.
Typically inlining small functions is what enables the compiler to unlock further optimisations. From simple things such as merging duplicate loads from same address, to auto-vectorization.
If you want inlining don't use message passing. Just define your function as if you were writing C. Simple. Use message passing when you need the dynamism.
> - The verbosity can be annoying at first, but it forces you to think hard about everything, and when I come back to Objective-C code years later, I've no issue remembering what the hell I was doing there
This this this. I love how it forces the verbosity. It makes it so easy to understand.
> - People complain about brackets, but they just... don't matter. It's a syntax. You either deal with it or don't - you don't see me writing Lisp because I find the syntax annoying, which is fine.
I really don't mind the brackets. If you structure your code right, is not even that big of a deal.
> - Message passing in ObjC is so optimized that it probably can't get much faster, and anyone acting like it's slow has a potentially skewed understanding of this
Not just this, but I love how you can send messages to nil and it doesn't crash, or you can define a method in code and it runs.
Reference counting is the slowest way of doing automatic memory management, the only thing about it is being the easiest to implement.
And it shows, Swift gets slammed on the ixy paper by all tracing GC languages, spending an huge amount handling counters.
It makes sense for Swift though, because it is the easiest way to integrate with Objective-C, given the failure of its GC experience , having to deal with C semantics and non-GC enabled frameworks, that lead to constant crashes.
Objective-C was designed by adding objects to C. So the base syntax is C. Then the idea was that the object-oriented bits were inside [], and the named arguments or message syntax came from smalltalk and was designed to make the language easer to read. Named arguments are very verbose, but they make the code much more readable.
Then much later Apple added the . syntax, which while more familiar to a lot of modern developers kind of broke the cleanliness that the syntax used to have.
It wasn't viable as v1, but now it's solid & clean.
Seems like it was a good opportunity to take a well understood language (C/C++/Obj-C) and after 30 years rebuild it ground-up into what it should be. Some constructs & workarounds just weren't going away without a clean-slate industry-wide fully-compatible restart.
Top problem now is getting developers to not force-unwrap optionals unless unless able to prove it won't cause a crash.
I occasionally use `!` as an escape hatch from the compiler's inability to reason about code correctness. Assert signatures in TS 3.7 will mitigate this issue significantly, but until then, I'm bangin'.
Yeah there are still many flaws in typescript's null checking, even in quite pedestrian code. The ! operator isn't evil, far from it, it exists so that the compiler can be more aggressive at checking nulls knowing that programmers can always override if it makes a mistake. I would never rewrite my code to avoid ! per se, I write it to be most readable to humans, if that doesn't please the compiler then so be it.
That's an interesting discussion, and one that will still keep happening.
At a given point, you can handle errors, but there's nothing you can do about them
It's good when languages (and developers) realize there are errors you should handle but there are errors you "shouldn't" because there's nothing you can do but bail out.
In almost all cases, there's a more appropriate way to indicate your expectations than using force-unwrap.
Even in those cases where you can't recover from a failure of those expectations, your code will be 1000x more legible if you demonstrate that you understand that the error could exist and what it might represent about the system as a whole. If you want to explicitly fire a fatal error after that because there's no other form of recovery, so be it.
Trying to capture all that in a "!" saves you a few LOC (or worse: minutes of reasoning) now but suggests somebody else might be pulling their hair out in frustration six months later. Please don't do that to someone.
I save force unwraps for things that will be caught at develop/testing time, like app images that are supposed to be in the binary or controls that are supposed to be wired up in Interface Builder. Otherwise, for me, using a force unwrap is a code smell.
You fix that by banning force unwraps outside of something like unit tests with a linter. We do it at work and I can't remember it ever being an issue.
That's so rare in practice that you should just write it out fully. If it's happening frequently in your app, you should probably rethink your architecture. As time goes on, I've realized why people think asserts are code smells of their own.
That’s fine IF they can prove it won’t crash or that a crash is appropriate (a la forced exit). Too many developers use it as a happy path shortcut, not considering the compiler is warning of possible serious problems.
Crashes & exits are not acceptable in production code, and I’ve had to fix too many of them.
I strongly disagree. I would rather an application crashed and provided me with an actionable crash report rather than it continuing on in some random state. (This isn't to say that you should ignore errors; it's just that if you don't think there should be an error at a certain location, I would really want to know about it if there is one.)
That's why force-unwrap is so bad: the compiler is telling you there's a risk, at a time you can do something sensible about it, and force-unwrap puts solving the problem off to the worst possible scenario. My production user base can't afford "actionable crash reporting" as a debugging tool vs compiler warnings; even 0.01% of users experiencing it would get very expensive.
If a thing can’t be null/nothing and there is nothing you can do it’s usually best to tear the world down. Otherwise in the best case you have a button that does nothing but in the worst case something bad happens. In the normal case the crash just moves. “Catching” programmer errors because a program that limps along looks better than one that crashes is a design decision I never understood.
Yes, and I'm usually happy the compiler is there to warn me. However, the compiler is not great at telling me why something might fail, which can make it difficult to provide decent error handling. And of course, there are certain places where the compiler just cannot know that a certain operation will not fail.
I've inherited an Swift app once that was written by the dev for whom it was the first Swift project. IUOs everywhere. And the biggest contributor to the crash count. Spend six months cleaning it up; the crash rate went down dramatically.
It is not wise some value will always be there when you do not control its source (e.g. API).
> I've inherited an Swift app once that was written by the dev for whom it was the first Swift project. IUOs everywhere.
Right, hence why I'm suggesting that they can be useful if you don't put them everywhere and aren't using them as a band-aid to make your code compile :)
You know, I used to think that but I've come to realize that I often just ended up writing messages that were not useful. For example, here is some code that I wrote a while back:
guard let data = notification.userInfo?["updatedItem"] as? Data else {
assertionFailure("Could not retrieve updated item")
return false
}
I am really being helpful here? If I see a crash on this line:
let data = notification.userInfo?["updatedItem"] as! Data
I get essentially the same information, except it's done in a less verbose and easier-to-discover way. Whereas the first one is like adding this kind of useless comment:
1. ! used here is hard to spot, it's just few pixels of difference from ?.
2. assertionFailure is not the same as force unwrap, because it crashes only in debug mode. Ideally you would log it with some analytics so developers know if problem occured, but doing nothing is usually better than crashing the app.
Objective C is good once its philosophy "clicks" with you, but any new development I would start in on Swift. Swift is a much nicer language, feels like compiled Python.
Same with Android, I would choose Kotlin before Java if it was my choice.
People still working with Objective C might be like I recently was: maintaining a sizable codebase which, unless Apple breaks something, porting to Swift is not justifiable to management.
There's nothing unconventional about for/in loops on Apple's platforms. Even in Objective-C, NSFastEnumeration has been the recommended default loop mechanism for a decade+ now.
What are "old-fashioned loops"? Does that mean C-style?
As an also Lisp programmer, I find Swift's limited (and fixed!) set of control flow constructs downright ancient. They added for-each loops, but that's it. We had more "modern control flow" in the 1980's.
Diversity in control flow constructs isn't necessarily a good thing. In imperative languages you see goto being discouraged and removed. In functional languages you see things like call-with-current-continuation discouraged.
It is to me. The alternative is implementing them ad-hoc in every function. (Or perhaps waiting another few decades for Swift to add them to its compiler.) Having used many languages at different points on the power spectrum, I remain unconvinced that there's any advantage to omitting abstraction capabilities and forcing programmers to deal with it.
Besides, both of those examples are essentially non-local jumps, and I'm not sure I'd describe them as "modern".
In a sense, Swift already allows diversity in control flow constructs, via closures and the trailing closure syntax. It's just somewhat awkward, and not flexible enough to implement, say, most of Lisp's ITERATE library. That's packed full of exactly the kinds of control flow constructs that I have to write out by hand in Swift every day.
Do you mean the c style for loops? The were removed in favour of iterator style loops (for foo in bar { ... }). Or are there some others I haven't noticed?
It's actually more curious that they didn't have iterator style loops from the beginning. It's a pretty standard feature in newer languages and even plenty of older languages are adding support for it.
Swift had iterator-style loops from the beginning. It also initially supported C-style "for (i = 0; i < j; i++)" loops, but they were removed very early on (because in practice, they're rarely used for anything except iteration, and iterator-style loops are much more readable and less error-prone).
To be fair, no language is going to be perfect. I don't mind them, but I remember how odd enumerations and mappings felt at first in Python. IMO, overall, the pros of Swift vastly outnumber the cons, when the alternative is Objective C.
As a language itself, I love Swift. Optionals, the guard statement, protocols and protocol extensions standout as awesome productive features. However, (at least 1 year ago, I'm on React Native now), the tooling around Swift was still crappy. Expect longer compile times, occasional hair tearing issues, broken autocompletion and a flickering syntax highlighting. Overall though these issues aren't enough to make me want to go back to Objective C.
It's a testament to how crappy my code is, but I find myself having to give it type hints too often, and it'll give up on spitting out warnings if the file gets too large. There's a perverse thought. I can bypass warnings-as-errors by making my code bad enough ;)
This typically happens at the layers where interop isn’t so good. Like using really old obj-c stuff. It’s worth reporting this to the swift project on github.
I started using Swift around 5 years ago, and I have barely written a line of Objective-C since. I like the philosophy of the language a lot, and I use it on other types of projects at well. I'm frustrated by the tooling and the cross-platform story. It's a great language to program in, but it can be painful to work with in terms of project fragility and dependency management.
I spend most of my time in Swift. All new features are developed in Swift. We've slowly been doing a conversion, since about Swift 3, and are about 95% of the way there.
We also have several legacy apps that are all ObjC, or 50% ObjC. There's not a ton of new feature development, but still updates.
For personal projects Objective C (Java for Android). I like them more. For commercial projects, I guess, Swift/Kotlin is the way to go, as it's new hotness.
Pure Swift for the new apps. We freak out when we see @ObjC or NSObject usage in our codebase. On a long enough timescale when Apple converts away from using Objective-C under the covers, our code will not need any modification.
Buttons, selector-based Notifications (though the block version is finally good, so we mostly just use that now), & gesture recognizers look like our biggest users of @objc. Basically all to support target/action.
A quick search through one of my apps shows that about 4% of functions are marked as @objc (and we're not using the old compatibility mode where more methods were implicitly @objc either).
@IBAction can replace @ObjC for all selectors. We put it a mandate in our code reviews. There is also NSObjectProtocol which forces (in most cases) devs to subclass NSObject.
Been using pure swift for quite some time now. There used to be some libs for which I'd have to make a bridging header, but lately there's fewer of those.
Some crypto code is also still Objective-C due to easier linking, but that'll probably change soon enough.
I don't consider ObjC to be a bad language in the slightest; in fact, I'll offer the (probably contrarian) opinion that it's one of the greatest languages of the past few decades.