Hi all. Following a previous post from 2016 and with Swift 5 around the corner, I would like to continue this tradition of sharing our "field testing" developing apps with Swift.
It's worth noting that 2018 has been surprisingly good on that front thanks to:
- Xcode 10 + the Swift toolchain that became very good in terms of language parsing speed, code indexing & coloring, incremental compilation times, overall robustness of the build system, and the debugging tools. It's the first year in a long time we've felt to be that productive with the tools. Thumbs up.
- Static libraries (Swift) that have arrived & are supported (2017 - Xcode 9b4). This was my #1 request in 2015-2016, and although it looked like it was a bit improvised the way it happened, it's there. Static libs are so useful to organize complex development project without being force into many inefficient micro-dylibs (code size b/c no stripping, load times). We can finally remove our hacks from the toolchain.
Thank you for that, it is appreciated daily!
Previous Swift advantages remain:
- it enables to detect many errors or code design problems very early (compile time)
- there are fewer unexpected crashes at runtime (when reading our crash reports)
- it is very well suited to high performance (static structs & collections), mathematical code (operators, protocols), where we would have used C++ previously (STL, templates, operators).
There remain things that could be improved though. This list isn't meant to be exhaustive, and part of it may even have been solved already with Swift 5 (please do tell!). It is what comes first to my mind when I ask myself the question "What could be improved in Swift?". Here's the list.
- Fixed-size arrays. It is still ineffective that we can't use fixed-size arrays in code as we do in C. Low-level programming needs this badly, be it network frames, fixed size math types, etc. We need the compiler to check on indexes, to allow stack allocation (unlike Array), and enumeration (set/get). After a few years of Swift evolution, I can't think of any excuse for not tackling this very basic use case.
C
struct Mat4 {
double vals[16];
}
for(int i=0; i<16; i++) { mat.vals[i] = 0.0 }
Swift
struct Mat4 {
var vals: (Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double)
}
mat.vals.0 = 0.0
mat.vals.1 = 0.0
mat.vals.2 = 0.0
ā¦
mat.vals.13 = 0.0
mat.vals.14 = 0.0
mat.vals.15 = 0.0
- Improved access levels. There is a common scenario that is impractical in the current situation: splitting a type implementation into multiple files. You typically do that to add specific functionality to a type that is unrelated to the rest (modularity), or to split implementation complexity into multiple pieces (complexity). We need some form of "type private" or scoped access level. Today we are forced to use public (or internal) level, that is, giving a public interface to enable private implementation. This messes up the whole concept of public interface / private implementation. The language shouldn't make us decide between a single large file, or exposing type internals like it does today.
- Namespaces: we often want to scope some code to some contexts as a way to organize it. Currently, modules do that but it is mostly implicit (you can't manually define them). When we want to do it explicitly, we have to use empty/fake structs, and structs in structs, etc. It works, but is a bit weird. The ability to create namespaces or modules (maybe not the same module as the one defined for libs) would be a great addition to better organize code. Could be linked to the previous point (access level), as we could for instance define a module for the implementation of a given type, where all implementation vars would be available (think module private).
- Module import/re-export rules. We were sometimes confronted with the import of a module that would re-export all of its imported modules which we consider as implementation details. For instance, A import B, B imports C, but A gets all the types of C in the code completion for instance. The reason was not clear, as we were careful not to expose C type in B's public API. It would make sense to have finer control about what gets re-exported (or not). Constraining dependency propagation can be important to manage the complexity of large projects.
- Better interface readability. Swift code can be a long stream with public and private funcs / vars intermixed. It can be hard to read, especially if we compare to Obj-C header files. In Obj-C we could craft those public API to make it easily parseable by humans: we dug into the complexity of Class.m and Class+Private.h when implementing the type, and then just with the easy & clear Class.h when using the type. The split required by the tools had a good effect on readability (OK, tbh, maintaining those was also a pain). In Swift, all is mixed like a soup, and it all relies on conventions. The tool (Xcode) can show the "interface", but it is just a filter, it is no more "crafted". Moreover, it is not customizable (I personally don't enjoy the extra spacing between funcs, I'd like a more compact representation). Not sure what is the best route, but better readability would be great.
- Bridging headers. We have started making/using hybrid static libraries made of Swift and Obj-C. Typically those were Obj-C libs that we augmented with new capabilities in Swift. There are edge cases we currently can't handle well, it's mostly linked to access levels (again), because there are some part of the library we want to keep private, some other that we want to expose, and same thing for Swift which can also expose API to Obj-C internally or externally with the caller of the lib. Currently, the only thing that works robustly is that the bridging header that we use inside the library has to be made public as well. That constraints the lib Swift code to only use the public Obj-C API of the lib. This is better than nothing, but still an important limitation for library development.
- The
if case let
(or is itif let case
, already forgotā¦) syntax is very hard to remember, and requires anif
block when there are associated values. Also, testing enum cases should be possible at all times. Reminds me of Obj-C block type syntax, which was a mess. That is solved now (good!), but it looks like when you push that thing down, another one pops up somewhere. Anyway, I'm in favor of a language I can use without having to read the manual for basic stuff. - Edge-case access level behaviors. I noticed that adding public func in an extension of a private protocol would keep these method public in the type adopting the protocol. This is useful. Is it a feature? It looks like an error that could be removed from a future compiler version. Also, it does not work with an initializer. This should definitely be part of a more global access level discussion.
- There are some strange syntax elements, like the comma in
if let x = xxx, x.a == true
. Here the comma becomes a logical and. In simple expression, there's no problem, but when this becomes more complex, you forget whether you have to use a comma or a&&
because their meaning is the same. - About nested funcs, the dependency on self (strong reference) is hidden, which is sometimes misleading. Closures on the other hand make it explicit. Not sure we need 2 different approaches here.
- Asynchronous programming (async/await, or even better). We need more powerful asynchronous programming tools. Crossed fingers for this so that we can remove lots of spaghetti code / code pyramids.
Thank you for reading.