SwiftUI的认识与使用
SwiftUI简介
SwiftUI是苹果推出的一个新的UI框架,它使用了声明的方式,通过视图,基础控件和布局控件来进行页面的开发。
SwiftUI具有跨平台性,一份SwiftUI代码可以同时跑在iOS、macOS、tvOS、watchOS平台上。
SwiftUI编写的页面代码更简洁,广泛使用链式调用。
SwiftUI视图和UIKit视图可以互相转换,对于将旧的项目过度到新布局方式比较友好。
SwiftUI的运行速度优于UIKit,他减少了界面的层次结构,因此可以减少绘制步骤,并且他完全绕过了CoreAnimation,直接进入Metal,可以有优秀的渲染性能。
其实声明式页面布局前端已经出现了很久,像React, Vue都是使用的声明式布局,声明式布局与命令式布局相比有很多优势,
如:单向数据流,双向数据绑定,只要数据状态改变使用了这些数据的视图就会自动更新等。
声明式布局是UI布局方式的未来,这次苹果从命令式编程过度到声明式编程算是一个大的进步。
设计模式
采用Struct组成的树形结构组织页面。叶子节点是基本控件。
这棵Struct树类似于React的抽象语法树,它会在编译阶段将这些描述信息翻译成真实的UIKit中的UI控件。
视图结构
APP根入口
APP的根入口是一个Struct结构体,它遵守APP协议
1 2 3 4 5 6 7 8 | @ main struct WorldLandMarkApp : App { var body : some Scene { WindowGroup { ContentView () } } } |
App协议
1 2 3 4 5 6 7 8 | public protocol App { associatedtype Body : Scene @ SceneBuilder @ MainActor var body : Self . Body { get } @ MainActor init () } |
页面结构体
some表示返回的是一个遵守了View协议的不透明类型,也就是var body: some View {} 这个计算属性中,只能return一种类型,不能出现if a {Text()} else {List{}} 这样的2种类型。
1 2 3 4 5 | struct LandmarkList : View { var body : some View { Text ( /*@START_MENU_TOKEN@*/ "Hello, World!" /*@END_MENU_TOKEN@*/ ) } } |
View协议
associatedtype Body : View 表示协议中定义了一个新类型Body,这个Body遵守View协议。
Self.Body 表示协议中的Body类型。Self表示类型本身,self表示实例变量本身
1 2 3 4 | public protocol View { associatedtype Body : View @ ViewBuilder @ MainActor var body : Self . Body { get } } |
每个页面swift文件中都有2个结构体,一个表示要开发的页面,另一个是使用Canvas进行展示出来的视图,其中struct ContentView_Previews: PreviewProvider可以根据Debug需要在外层嵌套导航条,展示Group组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import SwiftUI struct ContentView : View { var body : some View { Text ( "Hello, world!" ) . padding () } } struct ContentView_Previews : PreviewProvider { static var previews : some View { ContentView () } } |
它们不一定保持一致,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct LandmarkList_Previews : PreviewProvider { struct DeviceType : Identifiable { var id = UUID () var name : String } static var previews : some View { //使用ForEach展示多个设备 ForEach ([ DeviceType ( name : "iPhone 12" ), DeviceType ( name : "iPhone 13" )]){ deviceItem in LandmarkList (). previewDevice ( PreviewDevice ( rawValue : deviceItem . name )) . previewDisplayName ( deviceItem . name ) } } } |
状态双向绑定
@State单页面状态绑定
通过@State修饰的变量是做了双向绑定的,如果这个变量数据发生了改变,所有使用这个变量的视图都会自动更新。但是@State的修饰范围是当前的一个视图,如果想一个状态修改,整个APP范围内使用这个变量的视图全部都更新,则需要使用全局环境变量的模式。
1 2 3 4 5 6 7 | @ State private var isOpen struct LandmarkList : View { @ State private var isOpen : Bool = false //@ObservedObject: 全局环境变量绑定 @ ObservedObject var userData : UserData = UserData () } |
@ObservableObject+@Published全局状态变量
要使用全局状态变量,则需要创建一个class,并遵守ObservableObject协议。 然后在这个类中定义一个@Published修饰的变量 @Published var userLandmarks, 当@Published修饰的变量更新时,那么使用了@Published修饰的变量的视图就会对应更新。
定义一个UserData,遵守ObservableObject协议
1 2 3 4 5 6 7 | import SwiftUI import Combine class UserData : ObservableObject { @ Published var userLandmarks :[ Landmark ] = landmarks } |
使用时,也要在对应的视图中加上 @ObservedObject修饰符,然后更新这个变量self.userData.userLandmarks[self.userLandmarkIndex].isFeatured.toggle()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct LandmarkDetail : View { var landmark : Landmark @ ObservedObject var userData : UserData var userLandmarkIndex : Int { userData . userLandmarks . firstIndex ( where : {$ 0 . id == landmark . id })! } var body : some View { Button ( action : { self . userData . userLandmarks [ self . userLandmarkIndex ] . isFeatured . toggle () }){ if landmark . isFeatured { Image ( "icon_rcxinhua_selected" ) . resizable (). frame ( width : 20 , height : 20 , alignment : . center ) } else { Image ( "icon_rcxinhua_defaultselected" ) . resizable (). frame ( width : 20 , height : 20 , alignment : . center ) } } } } |
单向数据流
用户操作导致@State变量发生了改变,
@State变量改变导致使用了@State变量的UI视图就会自动更新
继续等待用户操作触发@State变量发生变化

UIKit控件与SwiftUI中控件的转换
UIKit转SwiftUI
通过UIViewRepresentable协议将UIView包装成SwifUI的View来使用。
1 2 3 4 5 6 7 8 9 | struct MapView : UIViewRepresentable { let view : UIView = UIView () func makeUIView ( context : Context ) - > some UIView { return view } func updateUIView ( _ uiView : UIViewType , context : Context ) { view . backgroundColor = . red } } |
SwiftUI转UIKit
通过UIHostingController将SwiftUI包装成UIView
1 | UIHostingController ( rootView : ContentView ()) |
Model模型定义
List,ForEach等要求被循环的每个元素都要有一个唯一的标识
这样数据变更时,可以迅速定位刷新对应的UI,提高性能
所以,元素要遵循Identifiable协议(实现id协议)
1 2 3 4 | struct Landmark : Identifiable { var id = UUID () let name : String } |
Demo地址
https://github.com/zhfei/SwiftBasicKnowledge
参考文章
https://www.jianshu.com/p/0f7215591b08
https://zhuanlan.zhihu.com/p/436779033
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
2017-07-31 小白读iOS冗余资源扫描脚本