美文网首页iOS开发
SwiftUI官方教程解读

SwiftUI官方教程解读

作者: iridescentzc | 来源:发表于2019-08-08 10:51 被阅读0次
    SwiftUI简介

    SwiftUI是wwdc2019发布的一个新的UI框架,通过声明和修改视图来布局UI和创建流畅的动画效果。并且我们可以通过状态变量来进行数据绑定实现一次性布局;Xcode 11 内建了直观的新设计工具canvus,在整个开发过程中,预览可视化与代码可编辑性能同时支持并交互,让我们可以体验到代码和布局同步的乐趣;同时支持和UIkit的交互

    设计工具canvus
    • 开发者可以在canvus中拖拽控件来构建界面, 所编辑的内容会立刻反应到代码上
    • 切换不同的视图文件时canvus会切换到不同的界面
    • 点击左下角的按钮钉我们可以把视图固定在活跃页面
    • 选中canvus中的控件command+click可以调出inspect布局控件的属性
    • 点击右上角的+可以获取新的控件并拖拽到对应的位置
    • 在live状态下我们可以在canvus中调试点击等可交互效果 但不能缩放视图大小
      每次修改或者增加属性需要点击resume刷新canvus
      landMarkDetail布局代码见布局部分
    文件结构

    创建一个SwiftUI文件,默认生成两个结构体。一个实现view的协议,在body属性里描述内容和布局;一个结构体声明预览的view 并进行初始化等信息,预览view是控制器的view时可以显示在多个模拟器设备,是控件view时可以设置frame,预览view是提供给canvus展示的,使用了#if DEBUG 指令,编译器会删除代码,不会随应用程序一起发布

    struct LandmarksList_Previews: PreviewProvider {
        static var previews: some View {
            ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
                LandmarkList()
                    .previewDevice(PreviewDevice(rawValue: deviceName))
                    .previewDisplayName(deviceName)
                  //.previewLayout(.fixed(width: 300, height: 70)) 设置view控件大小
            }
            .environmentObject(UserData())
        }
    }
    #endif
    
    布局

    普通的view:将多个视图组合并嵌入到堆栈中,这些堆栈将视图水平、垂直或者前后组合在一起

    VStack {  //这里的布局实现的是上图canvus中landMarkDetail的效果
                MapView(coordinate: landmark.locationCoordinate)
                    .frame(height: 300)//不传width默认长度为整个界面
                CircleImage(image: landmark.image(forSize: 250))
                    .offset(x: 0, y: -130)
                    .padding(.bottom, -130)
                VStack(alignment: .leading) {
                    Text(landmark.name)
                        .font(.title)
                    HStack(alignment: .top) {
                        Text(landmark.park)
                            .font(.subheadline)
                        Spacer() //将水平的两个控件撑开
                        Text(landmark.state)
                            .font(.subheadline)
                    }
                }
                .padding()
                Spacer()
            }
    

    列表的布局:要求数据是可被标识的
    (1)唯一标识每个元素的主键路径

     List(landmarkData.identified(by: \.id)) { landmark in
                LandmarkRow(landmark: landmark)
            }
    

    (2)数据类型实现Identifiable protocol,持有一个id 属性

    struct Landmark: Hashable, Codable, Identifiable {
        var id: Int  //
        var name: String
        fileprivate var imageName: String
        fileprivate var coordinates: Coordinates
        var state: String
        var park: String
        var category: Category
    }
      List(landmarkData) { landmark in
                LandmarkRow(landmark: landmark)
            }  //直接传数据源
    
    导航

    添加导航栏是将其嵌入到NavigationView中,点击跳转的控件包装在navigationButton中,以设置到目标视图的换位。navigationBarTitle设置导航栏的标题,navigationBarItems设置导航栏右边的item

      NavigationView {//显示导航view
                List {
                      //SwiftUI里面的类似switch的控件,可以在list中直接组合布局
                     Toggle(isOn: $showFavoritesOnly) {
                        Text("Favorites only")
                     }
                    ForEach(landmarkData) { landmark in
                        if !self.showFavoritesOnly || landmark.isFavorite {
                             //跳转到地标详细页面
                            NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                                LandmarkRow(landmark: landmark)
                            }
                        }
                    }
                }
                .navigationBarTitle(Text("Landmarks"))//导航标题
            }
        }
    

    实现modal出一个view

      .navigationBarItems(trailing:
                   //点击navigationBarItems modal出profileHost页面
                    PresentationButton(
                        Image(systemName: "person.crop.circle")
                            .imageScale(.large)
                            .accessibility(label: Text("User Profile"))
                            .padding(),
                        destination: ProfileHost()
                    )
                )
    

    程序运行是从sceneDelegate定义的根视图开始的, UIhostingController 是UIViewController的子类

    动画效果

    SwiftUI包括带有预定义或自定义的基本动画 以及弹簧和流体动画,可以调整动画速度,设置延迟,重复动画等等
    可以通过在一个动画修改器后面添加另一个动画修改器来关闭动画

    • 转场动画
      系统转场动画调用: hikeDetail(hike.hike).transition(.slide)
      自定义的转场动画:把转场动画作为AnyTransition类的类型属性 (方便点语法设置丰富自定义动画)
    extension AnyTransition {
        static var moveAndFade: AnyTransition {
            let insertion = AnyTransition.move(edge: .trailing)
                .combined(with: .opacity)
            let removal = AnyTransition.scale()
                .combined(with: .opacity)
            return .asymmetric(insertion: insertion, removal: removal)
        }
    }
    

    HikeDetail(hike: hike).transition(.moveAndFade)调用转场动画;move(edge:)方法是让视图从同一边滑出来以及消失;asymmetric(insertion:removal:)设置出现和小时的不同的动画效果

    • 阻尼动画
    var animation: Animation {  //定义成存储属性方便调用
            Animation.spring(initialVelocity: 5)//重力效果,值越大,弹性越大
                .speed(2)//动画时间,值越大动画速度越快
                .delay(0.03 * Double(index))
        }
    
    • 基础动画
                    Button(action: //点击按钮显示一个view带转场的动画效果
                        withAnimation {
                            self.showDetail.toggle()
                        }
                    }) {
                        Image(systemName: "chevron.right.circle")
                            .imageScale(.large)
                            //旋转90度
                            .rotationEffect(.degrees(showDetail ? 90 : 0))
                            //.animation(nil) //关闭前面的旋转90度的动画效果,只显示下面的动画
                           //选中的时候放大为原来的1.5倍
                            .scaleEffect(showDetail ? 1.5 : 1)
                            .padding()
                          //  .animation(.basic()) 实现简单的基础动画
                            //.animation(.spring()) 阻尼动画
                        
                    }
    

    给图片按钮加动画效果, 对应的会有旋转和缩放会有动画;加到action时,即使点击完成后的显示没有给image的可做动画属性加动画效果,全部都有动画,包含旋转缩放和转场动画

    数据流

    利用SwiftUI环境中的存储 ,把自定义数据对象绑定到view ,SwiftUI监视到可绑对象任何影响视图的更改并在更改后显示正确的视图

    • 自定义绑定类型
      声明为绑定类型 BindableObject ,PassthroughSubject是Combine框架的消息发布者, SwiftUI通过这个消息发布者订阅对象,并在数据发生变化的时候更新任何需要刷新的视图
    import Combine
    import SwiftUI
    final class UserData: BindableObject {
        let didChange = PassthroughSubject<UserData, Never>()
        
        var showFavoritesOnly = false {
            didSet {
                didChange.send(self)
            }
        }
    
        var landmarks = landmarkData {
            didSet {
                didChange.send(self)
            }
        }
    }
    

    当客户机需要更新数据的时候,可绑定对象通知其订阅者
    eg:当其中一个属性发生更改时,在属性的didset里面通过didchange发布者发布更改

    • 绑定属性
      (1)state
    @State var profile = Profile.default
    

    状态是随时间变化影响页面布局内容和行为的值
    给定类型的持久值,视图通过该持久值读取和监视该值。状态实例不是值本身;它是读取和修改值的一种方法。若要访问状态的基础值,请使用其值属性。
    (2)binding

    @Binding var profile: Profile//向子视图传递数据
    

    (3)environmentObject :

    @EnvironmentObject var userData: UserData
    

    存储在当前环境中的数据,跨视图传递,在初始化持有对象的时候使用environmentObject(_:)赋值可以和前面的自定义绑定类型一起使用

    let window = UIWindow(frame: UIScreen.main.bounds)
             window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
    
    • 绑定行为
      是对可变状态或数据的引用,用$的前缀访问状态变量或者其属性之一实现绑定控件 也可以访问绑定属性来实现绑定
    与UIkit的交互

    表示UIkit的view和controller 需要创建遵UIViewRepresentable或者UIViewControllerRepresentable协议的结构体,SwiftUI管理他们的生命周期并在需要的时候更新
    实现协议方法:

    //创建展示的UIViewController,调用一次
    func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
    //将展示的UIViewController更新到最新的版本
     func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)
    //创建协调器
     func makeCoordinator() -> Self.Coordinator
    

    在结构体内嵌套定义一个coordinator类。SwiftUI管理coordinator并把它提供给context ,在makeUIView(context:)之前调用这个makeCoordinator()方法创建协调器,以便在配置视图控制器的时候可以访问coordinator对象
    我们可以使用这个协调器来实现常见的Cocoa模式,例如委托、数据源和通过目标操作响应用户事件

    这里以用UIPageViewController实现轮播图为例,要注意其中的更新页面的逻辑~

    pageview作为主view,组合一个PageControl 和 PageViewController实现图片轮播效果
    PageView: @State var currentPage = 1 定义绑定属性 ,$currentPage实现绑定到PageViewController
    PageViewController: @Binding var currentPage: Int 定义绑定属性,在更新的方法updateUIViewController里面绑定显示,点击pagecontrol的更新页面时pageviewcontroller可以更新到最新的页面
    pagecontrol: @Binding var currentPage: Int定义绑定属性 ,updateUIView 绑定显示,pageview滑动更新页面 pagecontrol可以更新到正确的显示

    struct PageView<Page: View>: View {
        var viewControllers: [UIHostingController<Page>]
        @State var currentPage = 1
    
        init(_ views: [Page]) {//传入的view用SwiftUI的controller包装好后面传给pagecontroller
            self.viewControllers = views.map { UIHostingController(rootView: $0) }
        }
    
        var body: some View {
            ZStack(alignment: .bottomTrailing) {//将currentpage绑定起来了
                PageViewController(controllers: viewControllers, currentPage: $currentPage)
                PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
                    .padding()
                 //Text("Current Page: \(currentPage)").padding(.trailing,30)
            }
        }
    }
    
    import SwiftUI
    import UIKit
    struct PageViewController: UIViewControllerRepresentable {
        var controllers: [UIViewController]
        @Binding var currentPage: Int
    
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        func makeUIViewController(context: Context) -> UIPageViewController {
            let pageViewController = UIPageViewController(
                transitionStyle: .scroll,
                navigationOrientation: .horizontal)
            pageViewController.dataSource = context.coordinator
            pageViewController.delegate = context.coordinator
    
            return pageViewController
        }
        func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
           //pageviewcontroller绑定currentpage显示当前的页面,pageView变化的时候,page更新页面
            pageViewController.setViewControllers(
                [controllers[currentPage]], direction: .forward, animated: true)
    
        }
        class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
            var parent: PageViewController
    
            init(_ pageViewController: PageViewController) {
                self.parent = pageViewController
            }
          //左滑显示控制
            func pageViewController(
                _ pageViewController: UIPageViewController,
                viewControllerBefore viewController: UIViewController) -> UIViewController? {
                guard let index = parent.controllers.firstIndex(of: viewController) else {
                    return nil
                }
                if index == 0 {
                    return parent.controllers.last
                }
                return parent.controllers[index - 1]
            }
           // 右滑动显示控制
            func pageViewController(
                _ pageViewController: UIPageViewController,
                viewControllerAfter viewController: UIViewController) -> UIViewController? {
                guard let index = parent.controllers.firstIndex(of: viewController) else {
                    return nil
                }
                if index + 1 == parent.controllers.count {
                    return parent.controllers.first
                }
                return parent.controllers[index + 1]
            }
            func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
                if completed,
                    let visibleViewController = pageViewController.viewControllers?.first,
                    let index = parent.controllers.firstIndex(of: visibleViewController) {
                   //当view滑动停止的时候告诉pageview当前页面的index(数据变化 pageview更新pagecontrol的展示)
                    parent.currentPage = index
                }
            }
        }
    }
    
    struct PageControl: UIViewRepresentable {
        var numberOfPages: Int
        @Binding var currentPage: Int
    
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    
        func makeUIView(context: Context) -> UIPageControl {
            let control = UIPageControl()
            control.numberOfPages = numberOfPages
            control.addTarget(
                context.coordinator,
                action: #selector(Coordinator.updateCurrentPage(sender:)),
                for: .valueChanged)
    
            return control
        }
    
        func updateUIView(_ uiView: UIPageControl, context: Context) {
            uiView.currentPage = currentPage
        }
    
        class Coordinator: NSObject {
            var control: PageControl
    
            init(_ control: PageControl) {
                self.control = control
            }
    
            @objc
            func updateCurrentPage(sender: UIPageControl) {
                control.currentPage = sender.currentPage
            }
        }
    }
    

    \color{rgb(150,90,150)}{QA}: 当我们编辑一部分用户数据的时候,我们不希望在编辑数据完成的时候影响到其他的页面 那么我们需要创建一个副本数据, 当副本数据编辑完成的时候 用副本数据更新真正的数据, 使相关的页面变化 这部分的内容参见demo中profiles的部分;对于画图的部分demo中也有非常酷炫的示例,详情参见 HikeGraphBadge(徽章)

    参考资料

    Apple官网教程 :https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
    demo下载
    SwiftUI documentation

    作者简介

    就职于甜橙金融(翼支付)信息技术部,负责 iOS 客户端开发

    相关文章

      网友评论

        本文标题:SwiftUI官方教程解读

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