1 缘起

我花了半年多的时间,在闲暇时间,学习了苹果的Swift语言和SwiftUI框架,想体验下IOS开发,再看下有没有机会通过写软件来做点副业。

先花了大概3个月时间,通过阅读 The Swift Programming Language 这本官方电子书1来学习Swift这门语言,又花了接近4个月的时候来学习 100 Days of SwiftUI 这门课程2,每天花费1到2小时来学习一课,总共100课,所以顾名思义叫 100 Days of SwiftUI, 课程非常新且好,讲师功力深厚,课讲得深入浅出,娓娓道来。

每完成一课,就在Twitter上发一条推文,今天刚好把第100天的推文发了.




今天是结课之日,我通过了结课的考试,总分100分,考了91分,喜提课程证书一枚.


在整个课程中,我写了19个IOS App(虽说大部分是功能简单的App), 源码也基本放在 GitHub 3上了,不过所有的App都没有上架App Store,因为我还没有给苹果交税(99美刀的开发者注册费).

经过这100节课和19个APP的训练,我自觉已经掌握了使用Swift和SwiftUI的基础开发技能,算是个入门的IOS开发了, 现在我可以说自己是前端,后端,数据开发,IOS开发都搞过的全栈()工程师了(不是)

但是在苹果对SwiftUI开发思路做出改变之前,我SwiftUI之旅可能就先到此为止了,原因下文再谈

2 Swift 初体验

Swift 是由LLVM之父 Chris Lattner 4在2010开始开发,在2014年的WWDC苹果开发者大会正式推出的一门编程语言。

按照官方的说法,Swift从 Objective-C, Rust, Haskell, Ruby, Python, C#身上都有不同程度的借鉴和学习。

因为我对上面提到的语言多少有涉猎,所以学习Swift起来基本没有什么困难, Optional, Error Handling, Result, Generic, Enumerations, Protocol 这些概念都和Rust的大同小异。

又是由LLVM之父来操刀,所以语言本身也设计得很优雅.

让我眼前一亮的可能是借鉴自 C# Extension Methods 5extension 功能 , 可以对已有的 class, enum 或者是 protocol 类型增加新的函数,也就是在不修改源码的情况下,扩展已有的功能.

例如,以下的代码就可以扩展内置的 Double 类型, 实现以米为单位,进行千米, 厘米,毫米,公尺的转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// Prints "One inch is 0.0254 meters"
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"

总体而言, Swift是一门吸收了众多PL理论的现代编程语言, 官方说支持Linux,Windows,MacOS等多个平台,不过我估计大多是在MacOS上用来写IOS和Mac应用

3 SwiftUI

SwiftUI 使用的声明式语法,让开发者写页面布局和效果变得简洁清晰, 例如通过 VStack, HStack, ZStack 就可以实现X轴,Y轴,和Z轴方向的布局

例如下面这个就是通过 ZStack 几行代码实现的叠加效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    let colors: [Color] =
    [.red, .orange, .yellow, .green, .blue, .purple]


var body: some View {
    ZStack {
        ForEach(0..<colors.count) {
            Rectangle()
                .fill(colors[$0])
                .frame(width: 100, height: 100)
                .offset(x: CGFloat($0) * 10.0,
                        y: CGFloat($0) * 10.0)
        }
    }
}


除了声明式语法之外,SwiftUI让人赏心悦目的就是动画。好的动画在App里面绝对能起到画龙点睛的作用,而SwiftUI的内置动画已经非常强大了,下面就是使用内置动画实现的动画效果:

 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
struct ContentView: View {
    @State private var dragAmount = CGSize.zero
    @State private var enable = false
    let letters = "Hello, World"
    var body: some View{
        HStack(spacing: 0) {
            ForEach(0..<letters.count, id: \.self) { index in
                Text(String(letters[letters.index(letters.startIndex, offsetBy: index)]))
                    .padding(5)
                    .font(.title)
                    .background(enable ? .green : .blue)
                    .offset(dragAmount)
                    .animation(.linear.delay(Double(index) / 20), value: dragAmount)
            }
        }.gesture(
            DragGesture()
                .onChanged {
                    dragAmount = $0.translation
                }
                .onEnded { _ in
                    dragAmount = CGSize.zero
                    enable.toggle()
                }
        )
    }
}


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  struct HeartBeatView: View {
    @State private var animationAmount = 1.0
    var body: some View {
        Button("SOS"){
        }
        .padding(50)
        .background(.red)
        .foregroundColor(.white)
        .clipShape(Circle())
        .overlay(
            Circle()
                .stroke(.red)
                .scaleEffect(animationAmount)
                .opacity(2 - animationAmount)
                .animation(.easeOut(duration: 1)
                    .repeatForever(autoreverses: false), value: animationAmount)
        )
        .onAppear {
            animationAmount = 2
        }
    }
}


而Xcode 15新增的预览功能也很好用,可以让开发者不需要启动iPhone模拟器就能预览页面效果,节省了非常多的等待时间。


4 问题

听起来好像很美好: IDE新功能好用,编程语言优雅, UI框架简洁好用; 但是苹果的开发思路却有问题: 苹果开发的SwiftUI不向后兼容老版本的IOS。

SwiftUI大部分功能都是只支持IOS16及以后的版本,而苹果新出来的数据持久框架 SwiftData 甚至只支持IOS17,
更离谱的是,SwiftUI的 BugFix 也只支持高版本IOS, 这就意味着用户不升级IOS版本,甚至SwiftUI的bug开发者都没法修复。

我自己的手机也只更新到IOS16,所以我时常会遇到我自己写的App没法运行到我自己手机上的情况。

不支持旧版本的IOS就让一大批的开发者和公司都没有动力去使用SwiftUI:

对于开发新应用的开发者而言,只支持IOS17就意味着会流失一大群使用IOS16及以下版本的用户,
而对于拥有存量用户的公司而言,更没有动力去使用SwiftUI,用了之后,旧版本IOS的用户可能直接无法打开应用。

因此SwiftUI就陷入了一个尴尬的境地,东西做得好,但是不会有人用;

没有人自然就不用有人分享,宣传这门技术,自然就导致相关的学习资料非常匮乏, 进一步加深了初学者的学习难度;

开发遇到问题连懂的人都不用,官方文档写了又约等于没有写, 直接劝退初学者,恶性循环。

又因为接受SwiftUI的开发者还不多,苹果版本迭代起来更加肆无忌惮,新版本又引入一堆的Breaking change,导致开发者更新版本非常痛苦.

另外一个问题就是SwiftUI与苹果现有框架整合得不够好,如 CoreImage 框架,顾名思义是用来作图片处理.

但之前是使用Objective-C写的,通过SwiftUI来调用,就会变成相当恶心,需要把Swift的数据结构传换成Objective-C来处理, 如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func applyProcess(){
    guard let outputImage = currentFilter.outputImage else {return}
    guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else{return}
    let uiImage = UIImage(cgImage: cgImage)
    processedImage = Image(uiImage: uiImage)
}

func loadImage() {
    Task{
        guard let imageData = try await selectedItem?.loadTransferable(type: Data.self) else {return}

        guard let inputImage = UIImage(data: imageData) else {return}

        let beginImage = CIImage(image: inputImage)
        currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
        applyProcess()
    }
}

CoreImage 框架的 CIImage 转成 CoreGraphics 框架的 CGImage, 然后再把 CGImage 转换成 UIKit 框架 UIImage, 然后再转换回SwiftUI 内置的 Image 类型, 可谓是相当麻烦了.

但是对比SwiftUI只支持高版本的问题,Objective-C和Swift的互操作问题也只能算是恶心,但是起码有解决方法,对于前者,开发者是完全没法自行解决.

5 总结

过了一把野生IOS开发的瘾,但是除非是苹果愿意让SwiftUI支持低版本的IOS,
不然我是没有太大意愿继续使用SwiftUI来开发IOS了,受众比较有限了。

想要支持低版本的IOS,就只能走UIKit和Objective-C这条历史老路,我对此着实是望而生畏,有空还是学习点其他有趣的东西。