美文网首页
关于SwiftUI的内部技术分享

关于SwiftUI的内部技术分享

作者: yyggzc521 | 来源:发表于2022-02-25 15:25 被阅读0次

    什么是 SwiftUI?[1]

    官方的定义非常明确:

    SwiftUI is a user interface toolkit that lets us design apps in a declarative way.
    SwiftUI 就是⼀种描述式的构建 UI 的⽅式。

    简介[2]

    苹果在 2019 WWDC 推出新一代声明式布局框架-SwiftUI ,该框架可用于 watchOS、tvOS、macOS、iOS 等,苹果的任意平台都可以使用,达到跨平台的实现。

    在 SwiftUI 出现之前,苹果不同的设备之间的开发框架并不互通,macOS 开发用的 AppKit,iOS 开发用的 UIKit,WatchOS 开发用的堆叠,每个都不一样,不能达到互通互用,可复用性差。

    之前

    import UIKit
    
    @main
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            // Override point for customization after application launch.
            return true
        }
    }
    

    现在

    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            Text("Hello, world!")
                .padding()
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
                .environmentObject(ModelData())
        }
    }
    

    分成两个部分:

    • struct ContentView 定义的是视图结构。
    • struct ContentView_Previews 是预览视图声明。
      我们主要关注第一部分:struct ContentView

    关键字 some ,其实就是一个opaque(不透明)类型,在返回类型前面添加这个关键字,代表你和编译器都确定这个函数总会返回一个特定的具体类型-只是你不知道是哪一种

    SwiftUI 的编辑器是双向交互的:

    • 左边代码编辑器的改动会立即反应到右边的预览视图。
    • 右边的预览视图的编辑也会同步到左边的代码视图。

    优点

    • 使用 SwiftUI,系统会默认支持白天和黑夜模式的自动切换
    • 实时刷新预览
    • 各种尺寸的屏幕间自动适配
    • 高效:更少的代码,更快的交付

    SwiftUI 1.0 基本没有公司敢用在正式上线的APP 上,API 在 Beta 版本之间各种废弃,UI 样式经常不兼容,大列表性能差

    缺点

    • iOS 14 才可放心的使用,
    • 要解决的是如何部署到低版本操作系统上?

    SwiftUI的基本组件[3]

    名称 含义
    Text 用来显示文本的组件,类似UIKit中的UILabel
    Image 用来展示图片的组件,类似UIKit中的UIImageView
    Button 用来展示图片的组件,类似UIKit中的UIButton
    List 用来展示列表的组件,类似UIKit中的UITableView
    ScrollView 用来支持滑动的组件,类似UIKit中的UIScrollView
    Spacer 一个灵活的空间,用来填充空白的组件
    Divider 一条分割线,用来划分区域的组件
    VStack 将子视图按“竖直方向”排列布局。(Vertical stack)
    HStack 将子视图按“水平方向”排列布局。(Horizontal stack)
    ZStack 将子视图按“两轴方向均对齐”布局(居中,有重叠效果)
    基本组件:
    • Text:用来显示文本的组件
    Text("Hello, we are QiShare!").foregroundColor(.blue).font(.system(size: 32.0))
    
    • Image:用来展示图片的组件
    Image.init(systemName: "star.fill").foregroundColor(.yellow)
    
    • Button:用于可点击的按钮组件
    Button(action: { self.showingProfile.toggle() }) {
        Image(systemName: "paperplane.fill")
            .imageScale(.large)
            .accessibility(label: Text("Right"))
            .padding()
    }
    
    • List:用来展示列表的组件
    List(0..<5){_ in
            NavigationLink.init(destination: VStack(alignment:.center){
                Image.init(systemName: "\(item+1).square.fill").foregroundColor(.green)
                Text("详情界面\(item + 1)").font(.system(size: 16))
        }) {
              //ListRow
           }
    
    布局组件:

    VStack、HStack、ZStack

    功能组件:
    • NavigationView:负责App中导航功能的组件,类似UIKit中的UINavigationView
    • NavigationLink:负责App页面 跳转 的组件,类似于UINavigationView中的 push与pop 功能
    NavigationView {
        List(0..<5){_ in
            NavigationLink.init(destination: VStack(alignment:.center){
                Image.init(systemName: "\(item+1).square.fill").foregroundColor(.green)
                Text("详情界面\(item + 1)").font(.system(size: 16))
        }) {
              //ListRow
           }
    }
    .navigationBarTitle("导航\(item)",displayMode: .inline)
    
    • TabView:负责App中的标签页功能的组件,类似UIKit中的UITabBarController
    TabView {
        Text("The First Tab")
            .tabItem {
                Image(systemName: "1.square.fill")
                Text("First")
            }
        Text("Another Tab")
            .tabItem {
                Image(systemName: "2.square.fill")
                Text("Second")
            }
        Text("The Last Tab")
            .tabItem {
                Image(systemName: "3.square.fill")
                Text("Third")
            }
    }
    .font(.headline)
    

    UI布局的基本法则

    在SwiftUI中,不能给子视图强制规定一个尺寸

    1. 父view为子view提供一个建议的size
    2. 子view根据自身的特性,返回一个size
    3. 父view根据子view返回的size为其进行布局
    举个例子: UI布局的基本法则
    struct ContentView: View {
        var body: some View {
            Text("Hello, world")
                .border(Color.green)
        }
    }
    
    1. ContentView是Text的父view,为Text提供一个建议的size(全屏尺寸)
    2. 然后Text根据自身的特性,返回了它实际需要的size
      (注意:Text的特性是尽可能的只使用必要的空间,也就是说能够刚好展示完整文本的空间)
    3. 然后ContentView根据Text返回的size,在其内部对Text进行布局,在SwiftUI中,容器默认的布局方式为居中对齐。

    Frame[4]

    frame 在UIKit中是一种绝对布局,它的位置是相对于父view左上角的绝对坐标。但SwiftUI中frame的概念却完全不同

    在SwiftUI中,frame是一个modifier(修饰符的意思),并不是真的修改了view。实际上会创建一个新的view

    举个例子

    struct ContentView: View {
        var body: some View {
            Text("Hello, world")
                .background(Color.green)
                .frame(width: 200, height: 50)
        }
    }
    
    想要的
    实际的

    在上边的代码中,.background并不会直接去修改原来的Text,而是在Text图层的下方新建了一个新的view

    为什么会这样呢?

    根据布局的3法则考虑这个问题

    在考虑布局的时候,是自下而上的!!!

    1. 我们先考虑ContentVIew,它的父view给他的建议尺寸为整个屏幕的大小
    2. ContentVIew去询问它的child,它的child为下边的那个frame,返回了width200, height50, 因此frame告诉ContentView它需要的size为width200, height50,因此最终ContentView的size为width200, height50
    3. background是个一个透明的view,它的父控件frame,给的建议尺寸是width200, height50。它又去询问其child,text返回的是只需要容纳文本的size,因此text的size并不会是width: 200, height: 50

    所以要想达到理想效果,需要修改一下上边的代码,调整frame和background的顺序就能实现

    struct ContentView: View {
        var body: some View {
            Text("Hello, world")
                .frame(width: 200, height: 50)
                .background(Color.green)
        }
    }
    

    数据处理的基本原则

    • Data Access as a Dependency 数据访问依赖

    SwiftUI中的界面是严格数据驱动的:运行时界面的修改,只能通过修改数据来间接完成,而不是直接对界面进行修改操作。不回再像 传统命令式编程 MVC 模式下那样,ViewController 承载各种 UIVew控件,开发者需要手动处理 UIView 和 数据之间的依赖关系。当数据产生变化时,要不停的同步数据和视图之间的状态变化。

    SwiftUI是一切皆 View,所以可以把 View 切分成各种细粒度的组件,然后通过组合的方式拼装成最终的界面,这种视图的拼装方式大大提高了界面开发的灵活性和复用性,视图组件化并任意组合的方式是 SwiftUI 官方非常鼓励的做法

    • A Single Source Of Truth 单一数据源

    在 SwiftUI 中,不同视图间如果要访问同样的数据,不需要各自持有数据,直接共用一个数据源即可。这样的好处是无需手动处理视图和数据的同步,当数据源发生变化时会自动更新与该数据有依赖关系的视图

    swiftUI数据流转规范 - 数据流图

    从上图可以看出SwiftUI 的数据流转过程:

    • 用户对界面进行操作,产生一个操作行为 action
    • 该行为触发数据状态的改变
    • 数据状态的变化会触发视图重绘
    • SwiftUI 内部按需更新视图,最终再次呈现给用户,等待下次界面操作

    数据流工具[5]

    通过它们建立数据和视图的依赖关系

    • Property
    • @State
    • @Binding
    • ObservableObject
    • @EnvironmentObject
    1. Property:
      开发中最常见的,它就是一个简单的属性,没什么特别。ChildView 需要 Parent View 给它传一个字符串,并且 ChildView 不对这个字符串进行修改,所以直接定义一个 Property,在使用的时候,直接让 Parent View 告诉它就好了。
    struct ContentView : View {
        var body: some View {
            ChildView(text: "Demo")
        }
    }
    
    struct ChildView: View {
        let text: String
        var body: some View {
            Text(text)
        }
    }
    
    1. @State:
    • 基于值类型的状态管理,这些值通常是字符串、数字、布尔等常量值
    • 只能在当前 View 的 body 内修改,所以它的使用场景是只影响当前 View 内部的变化
    • 当被@State包装的属性改变,SwiftUI 内部会自动重新计算和绘制 View的body部分
    • 被@State包装的变量一定要用private修饰,并且这个变量只能在当前view以及其子View的body中使用,不让外部使用。如果想让外部使用,则应该使用@ObservedObject和@EnvironmentObject
    struct PlayerView : View {
        @State private var isPlaying: Bool = false
        
        var body: some View {
            VStack {
                
                Button(action: {
                    self.isPlaying.toggle()
                }) {
                    Image(systemName: isPlaying ? "pause.circle" : "play.circle")
                }
            }
        }
    }
    
    1. @Binding

    传统的命令式编程中最复杂的部分莫过于状态管理,尤其是多数据同步。
    一个数据存在于不同的 UI 中,某个数据改变就要同步到不同的UI 中。当这样需要同步的数据变的很多,再加上一些其他的异步的操作和逻辑处理,会使代码变得臃肿、可读性下降,并且伴随着而来的就是各种 Bug,SwiftUI 的解决办法就是使用 @Binding

    使用@state包装的属性只在它所属view的内部使用,那么当它的子视图要访问这个属性的时候就要用到@binding了

    @Binding主要有下面几个作用

    • 在不持有数据源的情况下,任意读取
    • 从 @State 中获取数据,并保持同步
    • 对包装的值采用传址而不是传值
    struct ContentView: View {
        // 用@State修饰需要改变的变量
        @State private var count: Int = 0
        
        var body: some View {
            VStack {
                Text("\(count)").foregroundColor(.orange).font(.largeTitle).padding()
                // $访问传递给另外一个UI
                CountButton(count: $count)
            }
        }
    }
    
    struct CountButton : View {
        // 用@State修饰,绑定count的值
        @Binding var count: Int
        
        var body: some View {
            Button(action: {
                // 此处修改数据会同步到上面的UI
                self.count = self.count + 1
                
            }) { Text("改变Count")
            }
        }
    }
    
    1. ObservableObject

    它的原理和RxSwift发布者和订阅者的模式类似

    • ObservableObject 是个协议,必须要类去实现该协议,适用于多个 UI 之间的同步数据

    • 在应用开发过程中,很多数据其实并不是在 View 内部产生的,这些数据可能是一些本地存储的数据,也可能是网络请求的模型数据,这些数据默认是与 SwiftUI 没有依赖关系的,要想建立依赖关系就要用 ObservableObject,与之配合的是还有@ObservedObject和@Published两个修饰符

    • @Published 修饰的属性一旦发生了变化,会自动触发 ObservableObject 的objectWillChange 的 send方法,刷新页面。这一步是系统帮我们默认实现的

    • ObservedObject:被观察的对象 ,告诉SwiftUI,这个对象是可以被观察的,里面含有被@Published包装了的属性

    • @ObservedObject包装的对象,必须遵循ObservableObject协议。也就是说必须是class对象,不能是struct。

    • @ObservedObject允许外部进行访问和修改

    class UserSettings: ObservableObject {
        // 有可能会有多个视图使用,所以属性未声明为私有
        @Published var score = 123
    }
    
    struct ContentView: View {
        @ObservedObject var settings = UserSettings()
    
        var body: some View {
            VStack {
                Text("人气值: \(settings.score)").font(.title).padding()
                Button(action: {
                    self.settings.score += 1
                }) {
                    Text("增加人气")
                }
            }
        }
    }
    

    有这样一个场景,A->B->C->D->E->F,A界面的数据要传递给F界面,假如使用@ObservedObject包装,需要一层一层传递。再有反向传值的话就更复杂,且容易出错。而使用@EnvironmentObject则不需要,直接在F界面,通过SwiftUI环境直接取出来就行。

    1. @EnvironmentObject 包装的属性是全局的,整个app都可以访问
    • 主要是为了解决跨组件数据传递的问题。
    • 组件层级嵌套太深,就会出现数据逐层传递的问题,@EnvironmentObject可以帮助组件快速访问全局数据,避免不必要的组件数据传递问题。
    • 使用基本与@ObservedObject一样,但@EnvironmentObject突出强调此数据将由某个外部实体提供,所以不需要在具体使用的地方初始化,而是由外部统一提供。
    • 使用@EnvironmentObject,SwiftUI 将立即在环境中搜索正确类型的对象。如果找不到这样的对象,则应用程序将立即崩溃,所以要 慎用
    class UserSettings: ObservableObject {
        @Published var score = 123
    }
    
    struct ContentView: View {
        
        @EnvironmentObject var settings: UserSettings
        
        var body: some View {        
            NavigationView{            
                VStack {
                    // 显示score
                    Text("人气值: \(settings.score)").font(.title).padding()
                    // 改变score
                    Button(action: {
                        self.settings.score += 1
                    }) {
                        Text("增加人气")
                    }
                    // 跳转下一个界面
                    NavigationLink(destination: DetailView()) {
                        Text("下一个界面")
                    }
                }
            }
        }
    }
    
    struct DetailView: View {
        
        @EnvironmentObject var settings: UserSettings
        
        var body: some View {
            VStack {
                Text("人气值: \(settings.score)").font(.title).padding()
                Button(action: {
                    self.settings.score += 1
                }) {
                    Text("增加人气")
                }
            }
        }
    }
    
    // 需要注意此时需要修改SceneDelegate,传入environmentObject
    window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
    
    • Property、 @State、 @Binding 一般修饰的都是 View 内部的数据。
    • @ObservedObject、 @EnvironmentObject 一般修饰的都是 View 外部的数据:
    • 网络或本地存储的数据
    • 界面之间互相传递的数据

    总结:

    1. View与View间的公用数据使用@State + @Binding。
    2. 多个View与Class间的公用数据:对View用@ObservedObject,让Class满足ObservableObject协议。
    3. 父View与子View对Class间的公用数据:父View用@ObservedObject,子View用@EnvironmentObject,Class满足ObservableObject协议

    与UIKit彼此相容

    由于SwiftUI 是一个新发布的框架,UI 组件并不齐全,当 SwiftUI 中并没有提供类似的功能时,可以把 UIKit 中已有的部分进行封装后,提供给 SwiftUI 使用。不过需要遵循UIViewRepresentable协议。UIViewRepresentable协议是SwiftUI框架中提供的用于将UIView转换成SwiftUI中View的协议
    当然,也可以在已有的项目中,仅用 SwiftUI 制作一部分的 UI 界面。
    UIViewRepresentable协议

    protocol UIViewRepresentable : View
        associatedtype UIViewType : UIView
    
       /// 返回想要封装的 UIView 类型 和 实例
        func makeUIView(context: Self.Context) !" Self.UIViewType
    
        /// UIViewRepresentable 中的某个属性发生变化,SwiftUI 要求更新该 UIKit 部件时被调用
        func updateUIView(
            _ uiView: Self.UIViewType,
            context: Self.Context
        )
    }
    

    举个栗子

    struct SearchBar : UIViewRepresentable {
        
        @Binding var text : String
        
        class Cordinator : NSObject, UISearchBarDelegate {
            
            @Binding var text : String
            
            init(text : Binding<String>) {
                _text = text
            }
            
            func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
                text = searchText
            }
        }
        
        func makeCoordinator() -> SearchBar.Cordinator {
            return Cordinator(text: $text)
        }
        
        func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
            let searchBar = UISearchBar(frame: .zero)
            searchBar.delegate = context.coordinator
            return searchBar
        }
        
        func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
            uiView.text = text
        }
    }
    

    学习资料

    1. 斯坦福公开课 CS193P·2020 年春:该课程强推,我当年学习 OC 看的就是它,现在到SwiftUI了还是先看这个,系统且细致,结合案例和编程过程中的小技巧介绍,是很好的入门课程。
    2. 苹果官方 SwiftUI 课程:打开Xcode,照着官方的教学,从头到尾学着做一遍应用。
    3. Hacking with swift:这是国外一个程序员用业余时间搭建的分享网站,有大量的文章可以阅读,还有推荐初学者跟着做的「100 Days of SwiftUI」课程。
    4. 苹果官方文档:虽然很多文档缺乏工程细节,但是文档涉及很多概念性的内容,你可以知道官方是怎么思考的,并且有很多具体的机制参数
    5. SwiftUI的 View 如何布局?

    1. https://www.zhihu.com/question/327763737

    2. https://sspai.com/post/65567

    3. https://juejin.cn/post/6844903999762595854

    4. https://www.jianshu.com/p/8b05d84fe411

    5. https://www.jianshu.com/p/72fc8a2f530f

    相关文章

      网友评论

          本文标题:关于SwiftUI的内部技术分享

          本文链接:https://www.haomeiwen.com/subject/merrfrtx.html