App Development Journey (Part 1): The Marvelous Mystery of Swift

✍🏼 Written on Mar 27, 2025    💡 Updated on Apr 8, 2025
❗️ Note: it has been days since this article was written, please be aware of its timeliness
🖥  Note:This series documents my journey of learning Apple app development as a web front-end developer.

Explanation

Someone asked why not JavaScript—because it's a scripting language without a type system, making it completely incomparable to Swift.

As a front-end developer, I have only a beginner-level understanding of C, Java, and Python, but I’ve reached an advanced level (self-assessed) in TypeScript (hereafter referred to as TS) in terms of features and usage. Therefore, some things that shocked me might seem like “just ordinary language features” to others, or even prompt questions like, “Are the designers of TypeScript geniuses?”

I won’t necessarily feign shock at every difference between Swift and TS, because some differences stem from TS’s own limitations, while others are commonplace in programming languages (e.g., distinguishing between Int and Double instead of having just one Number type). It’s JavaScript’s design that’s truly shocking (after all, it was hastily designed in a week), not Swift’s.

My belief is: If a language has too many rules, too many exceptions, or too many reserved words, it’s not a good language.

Preface

This article follows the order of Swift’s official language introduction, focusing only on the parts that shocked me while omitting those that didn’t or that I didn’t understand, such as 附加宏.

Basics

Comments

Shocking discovery: Xcode doesn’t have a `/** */` block comment shortcut—only `cmd + /` for line comments.

VSCode’s block comment shortcut is also awkward to press: Alt + Shift + A. Do people not use this feature often?

Of course, in both VSCode and Xcode, you can automatically generate block comments by pressing /** and then Enter.

Optional Binding

Shocking discovery: While debugging, I wanted to write a value that’s always `true` in `if` for testing, but it’s not allowed—the optional binding in `if` statements must be an optional value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
if let a = 2 { } // 错误!
```

Is this really necessary?

## Collection Types

{% render_quote color="" %} Shocking point: Among array methods, there's `sort` for in-place sorting, and another `sorted` that returns a new array. {% endrender_quote %}

A language doing what frameworks typically do—very fitting for Apple's style. There are many such examples, but let's not dwell on each one.

## Control Flow

### if Expressions

{% render_quote color="" %} Shocking point: To address the issue of `if-else` assigning values to the same variable, it provides the `if` expression syntax. Similarly, `switch` also has an analogous expression form. {% endrender_quote %}

Swift really goes above and beyond:

```swift
// 平平无奇的写法
let a = 25
let str: String
if a <= 0 {
str = "小"
} else if a >= 30 {
str = "大"
} else {
str = "中"
}
// 简写法
let str = if a <= 0 {
"小"
} else if a >= 30 {
"大"
} else {
"中"
}

Switch’s Powerful Matching

Shocking point: switch has no implicit fallthrough.

This is understandable and more reasonable—who in their right mind would want case 1 to automatically spill into case 2 without explicitly using break?

Shocking point: switch can match object values!

This is actually considered feature, and intuitively more reasonable—truly an expert’s approach.

In TypeScript, since the switch case statement uses strict equality (===) for comparison, you generally wouldn’t pass an object inside your switch parentheses. This is because objects are compared by reference, and in TypeScript, there are rarely situations where you need to check object equality, let alone using a switch statement for such checks.

However, in Swift, you can “capture” the matched value (officially termed a “pattern”) within a case statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let somePoint = (1, 1) // 一个平平无奇的元组罢了
// 我现在,开 始 判 断:
switch somePoint {
case (0, 0):
print("\(somePoint) 在原点")
case (_, 0): // 忽略第一个值
print("\(somePoint) 在 x 轴")
case (0, _): // 忽略第二个值
print("\(somePoint) 在 y 轴")
case (-2...2, -2...2): // 判断元组的两个值是否分别在给定区间,在就匹配成功
print("\(somePoint) 在盒子内部")
default:
print("\(somePoint) 在盒子外部")
}

// 还可以值绑定!
switch somePoint {
case (let x, 0): // 匹配第二个值,捕获第一个值
print("x轴值:\(x)")
case (0, let y): // 匹配第一个值,捕获第二个值
print("y 轴值:\(y)")
case (let x, let y): // 兜底,因为 x 和 y 都是 let,还可以写作 case let (x, y)
print("\(x)\(y)")
}
// 还可以加 where 限定:
case let (x, y) where x == y:
//还可以多个匹配
case "a", "b", "c":

It’s worth noting that TypeScript also supports “multiple matches,” but this is entirely different from Swift’s case-based multiple matching.

In Swift, for multiple matches separated by commas, the case statement executes as long as any one of the comma-separated items matches. In contrast, TypeScript’s comma-separated matching essentially only matches the last item, because comma-separated expressions in TypeScript only return the final value:

1
2
3
4
5
var a = "d"
switch (a) {
case "d", "c", "b":
console.log("OK")// 此处不会执行,因为此 case 匹配 "b"
}

Functions

Labeled Parameters for Functions

Shocking Point: Formal parameters (labeled parameters) can share the same name??? And the rule (here comes another restriction, sigh): parameters following a variadic parameter must have argument labels (if there's only one, the actual argument becomes the formal parameter) and cannot be omitted.

1
2
3
4
5
6
7
// 下面两个 a 标签参数,完全合法(因为是 label 无所谓)
// 而且 a b 后面的参数 a c(或者单独的一个参数)必须存在,不能用 _ 省略,
// 也可以理解,要不咋知道是第二个参数,而不是前面的可变参数来的?
func a (a b: Double..., a c: Double) -> Double {
return b.reduce(0, +) + c
}
a(a: 1, 2, 3, 4, 5, a: 6)

Closures

N Abbreviated Forms of Closures

Shocking Point: There are too many syntactic sugars for closures to list here, but the most outrageous is the form that requires only a `>` symbol.

The essence of why this works is that String has a function called >. Yes, you read that right—a symbol can also be a function! More on this shocking detail later.

1
2
var num = ["9", "2", "1"]
var sortedNum = num.sroted(by: >)

Absolutely unbelievable.

Omitting Return Statements

Shocking Point: You might understand "single-line returns can omit `return`," but you'll never comprehend "multi-line returns can also omit `return`."

In TS, omitting the return statement works the same as in Swift’s ordinary syntax—single-line returns omit return:

1
2
3
4
5
6
// 省略写法
var a = () => 2
// 正常写法是:
var a = () => {
return 2
}

Ordinary Swift syntax:

1
2
3
func a()-> Int {
2 // -> 因为单行,所以省略了 return
}

But! The SwiftUI way of writing it:

1
2
3
4
VStack {
Text("Hello")
Text("World")
}

You read that right—this is also a trailing closure function, where VStack is a function. The closure parameter, being the last and only parameter, allows omitting the parentheses of VStack. But! It returns two Text function calls without writing return. Why?

Because it uses ViewBuilder, which shares the same syntactic sugar as resultBuilder later.

Auto-Closures

Shocking Point: At first glance, assigning a plain closure seems unremarkable, but its lazy evaluation capability left me utterly shocked.

When I first encountered auto-closures, they were introduced with this example, touting their “lazy evaluation” capability:

1
2
3
4
5
6
7
8
9
10
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印 “5”
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印 ”5“
print("Now serving \(customerProvider())!")
// 打印 “Now serving Chris!”
print(customersInLine.count)
// 打印 “4”

I thought, “Isn’t this just a plain closure, similar to TS, where the customerProvider variable is assigned a closure?” The same implementation in TS looks like this:

1
2
3
let customerProvider = () => {
// 省略逻辑
}

Of course, the closure’s logic is only executed when it is called—what kind of lazy evaluation is this!

But its lazy evaluation truly shines when closures are passed as parameters:

A plain and straightforward explicit closure invocation, similar to how it’s done in TS, where the closure is passed as a parameter and written within a pair of curly braces:

1
2
3
4
5
6
// customersInLine 是 ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印 “Now serving Alex!”

However, once you mark customerProvider as @autoclosuer, things become different. In this case, your serve function call can be written like this:

1
2
3
4
5
6
// customersInLine 是 ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印 “Now serving Ewa!”

Note the invocation of the serve function at the end. Its parameter customer is a statement customersInLine.remove(at: 0). In TS, regardless of the scenario, the call stack would first evaluate this value before invoking the serve function. But in Swift, this syntax is merely a different form of the above—the logic remains the same. That is, the serve function is executed first, and the statement is executed internally only when customerProvider is called. This is what they call 延迟计算, right?

If you write this often, you’ll encounter many cases like the following code:

1
2
a(b.remove(2))
c(d.add4())

At this point, it becomes hard to tell whether the statements inside the parentheses or the outer function are executed first. As a result, the Swift official documentation also provides a warning:

Overusing autoclosure can make your code difficult to understand. The context and function name should clearly indicate that evaluation is being deferred.

This is really absurd—if it’s not recommended, why design it in the first place?!

Enums

Shocking fact: In other languages, enums are merely a convenient, optional type for state machine checks, but in Swift, they are a first-class type and one of the most commonly used features—even capable of replacing Structs in certain scenarios. Can you believe it?

Shocking fact 2: Enums are value types.

In TS, enums are just plain old “enums,” nothing more than listing values, mostly used to describe states in state machines:

1
2
3
4
enum A {
B = "B"
C = "c"
}

Of course, the tricks TS plays with runtime and compile-time differences are another story. In TS, you could easily replace an enum with an object:

1
2
3
4
5
6
7
8
9
const enum A {
B = 0,
C = 1,
}

const A = {
B: 0,
C: 1,
} as const;

Associated Values

Shocking Point: Swift, What Were You Thinking Designing Enums Like This?!

In Swift, enums hold paramount importance. You can iterate through all their cases (requiring conformance to the CaseIterable protocol), treat a case as a function, and pass values during invocation for the enum instance to process—this is called “associated values”:

1
2
3
4
enum A {
case b(Int, Int) // 声明方式好像一个协议(也就是抽象类)
case c(String)
}

Then, when using it, you can pass values:

1
let a = A.b(1024, 9527)

Enums are most commonly used in switch statements. Combined with Swift’s surprisingly powerful case matching, you can do this:

1
2
3
4
5
6
switch a {
case .b(let d, let f):
print("A.b 关联值为:\(d) 和 \(f)")
case let .c(g): // 上面提到的另一种 switch case 写法
print("A.c:\(g)")
}

The first time I saw this, I found it quite abstract and couldn’t think of any practical use cases (since I’d never done it this way).

Implicit Assignment

Shocking Point: The type declaration in enums doesn't specify the enum's own type (key-value pairs) but rather the type of its cases.

Implicit assignment in Swift is consistent with TypeScript or other C-like languages: the first value is n, and subsequent values are n + 1.

But Swift goes a step further—it performs implicit assignment based on the type you declare, which isn’t the default behavior. For example:

1
2
3
4
5
6
enum A: Int { // <- 声明了 Int 类型,所以隐式赋值了
case b, c, d // b c d 分别为 0 1 2(需要使用 A.b.rawValue 才能访问,这又是另一个震惊点了)
}
enum B: String { // <- 声明了 String 类型,所以隐式赋值了
case bb, cc, dd // bb cc dd分别为 "bb" "cc" "dd"
}

In TypeScript, you can’t even specify the Int type that Swift allows (this type actually refers to the case’s type). Instead, you can only specify the enum’s type (usually key-value pairs Record elsewhere).

Structs and Classes

Shocking Point: Structs are actually value types?!

This seemingly straightforward struct is actually a value type (using Immutable optimizations for performance):

1
2
3
4
struct a {
var b: Int
var c: String
}

Something wrapped in curly braces doesn’t look like a value at all! Bizarre! Shocking!

Properties

Various Property Wrappers/Observers

Shocking Point: Swift does way too much. Concepts like computed properties, stored properties, property wrappers (which TypeScript also has), and property observers (TypeScript has these too, bundled with wrappers) are typically framework-level features, such as Vue's `computed` and `watch`. Yet Swift implements them directly in structs and classes—unbelievable!

1
2
3
4
5
6
struct A {
var b = 2 // <- 存储属性,直接有值,在 struct 实例化的时候就确定了
var c: Int {
return b * 2 // <- 一个只读计算属性,不存储值,因为单行,return 可以省略
}
}

Note: Property wrappers are not available for global variables.

Subscripts

Shocking Point: Honestly, the mere existence of the "subscript" concept is shocking enough. It provides a shortcut for accessing elements in collections, lists, or dictionaries. What's more, subscripts can even accept multiple values, just like functions.

Essentially, subscript calls can be thought of as function calls on data structures (like the aforementioned collections, lists, or dictionaries) to access values. The difference is that function calls use (), while subscripts use [].

Inheritance

Shocking Point: Similar to all the previous shocking points, Swift seems to casually add keywords for every possible purpose or effect. As a result, the number of reserved words (though they can't strictly be called "reserved" since Swift allows using any keyword by wrapping it in backticks—more on that later) related to inheritance is overwhelming. Examples include: `final`, `override`, `open`, `required`, and so on.

Initialization

Memberwise Initializers for Structs

Shocking Point: Because structs are value types, their initialization rules differ from those of traditional classes.

At first glance, structs and classes are just ordinary data structures, no different from classes in TypeScript. But once again, Swift complicates things here.

Since Struct is a value type, it’s special in that it has a constructor form called the “memberwise initializer.” This means when a Struct has no init at all, its properties can be initialized by passing parameters without explicitly writing them out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 此 Struct 没有 init,因此适用逐一成员构造器
struct A {
var a: Int
var b: Int
}
// 上述效果等同于:
struct A {
var a: Int
var b: Int
init(a: Int, b: Int) {
self.a = a
self.b = b
}
}
// 上面两个都可以这么用:
A(a: 1, b: 2)

However, this rule doesn’t apply to Classes. A Class must explicitly have an init constructor method to initialize its properties.

Designated Initializers and Convenience Initializers for Classes

Shocking point: What? There are two types of initializers???

The designated initializer of a class functions just like a regular construct in TS—it’s a init method. But its convenience initializer… is marked by adding the convenience keyword before init (here we go again!):

1
convenience init() {}

Actually, the Swift documentation doesn’t clearly explain to readers why convenience initializers are needed. Instead, it dives straight into class construction, inheritance, and initializer delegation. Here, I’ll explain to beginners why convenience initializers exist—for a very simple reason: 类中的多个指定构造器不允许互相调用。. That’s it! Can’t resist the urge to call it? Want to simplify the initialization process? Go ahead and use a convenience initializer!

Standard approach:

1
2
3
4
5
6
7
8
9
10
11
class A {
var b: Int
var c: String
init(b: Int, c: String) {
self.b = b
}
init() {
self.b = 0
self.c = "UnKnown"
}
}

Doesn’t it feel repetitive writing self.b twice like this? So, in the second init(), you might want to do this:

1
2
3
init() {
self.init(b: 0, c: "UnKnown")
}

Sorry, the compiler throws an error: Designated initializer for 'A' cannot delegate (with 'self.init'); did you mean this to be a convenience initializer?

In this case, you must use a convenience initializer:

1
2
3
converience init() {
self.init(b: 0, c: "UnKnown")
}

All for the sake of “convenience”!

Failable Initializers

Shocking point: Initializers can fail???

A so-called failable initializer simply means the initialization process (the init function call) might throw an error.

Therefore, if the initializer throws an error, you don’t need to wrap it in a try-catch like in TS. Instead, you can directly indicate it by adding a question mark after init, turning it into init?. Then, at the point where an error might occur, return nil, and when instantiating, check if it’s nil. That’s it!

In Swift, normal constructors don’t return values, which is the same as in TypeScript—but TypeScript constructors can return values. If the return value is an object type, it replaces the this object, which seems even more shocking.

Optional Chaining

Shocking point: Q: Under what circumstances does a function have parentheses written but not execute? A: In the context of optional chaining.

Without further ado, let’s look at an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func A() -> Int {
print("OK")
return 1
}
class B {
var c: C?
}
class C {
var d: Int?
}
var d = B()
// 震惊的事情发生了:
// 这里,A 函数未执行,不会打印 OK,赋值也不会成功,因为 c 是 nil,这个语句返回 nil(通常 Swift 的赋值语句都返回 Void 的,也就是空元组)
// 返回 nil 意味着你可以使用 if-let 语句判断
d.c?.d = A()

Error Handling

Shocking point: Like enums, Swift's try-catch (actually do-catch) is designed to be quite complex.

The catch statement can not only capture arbitrary errors but also specific error types. Using is or similar to a switch statement, there can be multiple catch branches for matching—absurd:

1
2
3
4
5
6
7
do {
try someError()
} catch is ErrorA {
// xxx
} catch ErrorB, ErrorC, ErrorD.a {
// xxx
}

Concurrency

Shocking point: While reading the documentation, I initially didn't realize that `withTaskGroup` is a built-in method for Group Task...

As for await, it’s used exactly the same way as await in TypeScript—so satisfying!

Extensions

Shocking point: Extensions might be the most powerful design in Swift. Any object, whether built-in or third-party, can be extended freely, at low cost, without any complex declarations or keywords.

Compared to TypeScript, where extending an existing class requires manipulating the prototype chain and then adjusting the object’s this to point to the class, in Swift, you just need a extension, and then you can boldly write any methods or properties you want—their this will point to the instance or the extended type (i.e., static methods or properties).

Absolutely mind-blowing.

Protocols

Shocking point: I believe protocols were introduced to address the abstraction issues of Structs, since in any language, classes themselves can be inherited (this might be an absolute statement), eliminating the need for an additional "protocol"—abstract classes would suffice. Incidentally, while Swift restricts classes to single inheritance, it allows protocols to play a more versatile role across classes, Structs, and enums.

Enough said—my thoughts are all above.

Generics

Shocking point: Generics introduced "associated types," which essentially serve the same purpose as generics declared upfront but are even more powerful.

Swift’s generic associated types look like this:

1
2
3
4
5
6
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

As for why it wasn’t designed like this:

1
2
3
4
5
protocol Container<Item> {
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

Presumably because the Item might become too lengthy and inelegant, such as:

1
2
3
4
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}

If written after the name, generic declarations would often exceed the screen width, which is unbearable.

Opaque Types and Protocol Wrapping

Shocking point: You can achieve an effect where only the compiler knows the concrete type, while the caller only knows it conforms to a certain protocol.

Essentially, opaque types and protocol wrapping aim to hide implementation details from callers while allowing code refactoring with minimal changes. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protocol Shape {
func area() -> Double
}

struct Circle: Shape {
var radius: Double
func area() -> Double {
return .pi * radius * radius
}
}

func makeShape() -> some Shape {
return Circle(radius: 5)
}

Here, makeShape can return any type that conforms to the Shape protocol, without explicitly specifying the Circle type. This way, if new types like Struct that conform to Shape are added later, the function can still return them (a common practice in SwiftUI, such as some View).

Memory Safety

Shocking point: Since Swift allows passing values by reference, it can lead to simultaneous read-write operations on the same memory region (which makes sense, right?).

1
2
3
4
5
var a = 1
func add(_ b: inout Int) {
b += a
}
add(&a)

The above code will crash at runtime, but the compiler won’t warn you. There are many similar cases where extra caution is needed. Since TypeScript doesn’t support pass-by-reference, such issues don’t arise—easy peasy.

Advanced Operators

Mind-blowing point: You can rewrite/customize the implementation of a function with a symbol as its name, thereby enabling operations between instances of the same class or structure.

For example:

1
2
3
4
5
6
7
8
9
10
11
struct A {
var b = 1
var c = 2
static func +(left: A, right: A) -> A {
return A(b: left.b + right.b, c: left.c + right.c)
}
}
// 然后你就可以
let a = A()
let b = A()
let c = a + b // <- 是的你没看错,struct 实例可以相加!

This design is brilliant—why didn’t I think of it? Amazing, truly amazing.

It’s worth noting that operator methods have their own attributes. For instance, + is a binary operator and also an infix operator, so it takes two functions. - can be either an infix operator (binary operator) or a prefix operator (unary operator, requiring prefix before func), making it overloadable.

However, Swift also specifies that the assignment operator = cannot be overloaded, nor can the ternary conditional operator (a ? b : c).

This operator is so common that you can find operator functions like == for equality checks in almost any Swift built-in object or Foundation object.

You can even implement your own operators!

1
2
3
4
5
6
7
8
9
10
11
// 需要先声明,这是一个操作符,因为有两个值在操作符,所以这是个中缀操作符(infix)
infix operator +++++++
// 然后扩展一下整数类型
extension Int {
static func +++++++(left: Int, right: Int) -> Int {
return (left + right) * 7 // <- 因为有 7 个 + 号,所以我写了乘以 7
}
}
// 这么用:
2 +++++++ 7 // <- 63

Absolutely incredible.

Result Builders

Mind-blowing point: Swift goes to extreme lengths for "elegance" and "reusability."

To make the following code look cleaner, Swift “invented” the magical @resultBuilder, as shown below:

Standard approach:

1
2
3
4
5
6
7
8
9
var a = 1
var b = 2

struct A {
var a: Int
}

// 这里会有一个三元判断
var c = A(a: a > b ? 666 : 999 )

Swift argues that the ternary conditional above is too cumbersome and becomes hard to read with complex logic, so it aims for elegance. Thus, result builders were born (if I understand correctly):

1
2
3
4
5
6
7
8
9
10
11
12
@resultBuilder
struct BBuilder {
static func buildBlock(_ b: Int) -> Int { // 对应 if/else 分支的执行结果
return b
}
static func buildEither(aaa: Int) -> Int { // 对应 if 分支
return aaa
}
static func buildEither(bbb: Int) -> Int { // 对应 else 分支
return bbb
}
}

Then, assign the c variable like this:

1
2
3
4
5
6
7
8
9
10
11
func BB(@BBuilder b: () -> Int) -> A {
return A(a: b())
}
// 终于等到 c 了,这里的 c 和最开始的 c 完全一样
var c = BB {
if a > b {
return 666
} else {
return 999
}
}

Reusable, elegant, efficient—brilliant!

Lexical Structure

Mind-blowing point: Despite Swift having an overwhelming number of reserved words, it allows you to use them as identifiers.

The method is simple: enclose them in backticks:

1
2
3
func `func`() {} // <- 一个叫做 `func` 的函数
// 调用也是
`func`()

However, apart from keywords, x and x refer to the same variable.

- EOF -
Originally published at: App Development Journey (Part 1): The Marvelous Mystery of Swift - Xheldon Blog