iOS 之 SwiftUI

1,974 阅读6分钟

简介

SwiftUIApple推出的现代界面框架,旨在简化界面开发和布局,并提供强大的响应式编程动画支持。与UIKit的一个最明显的区别便是:SwiftUI是一个纯coding式的框架,摒弃了UIkit的结合图形界面设计工具构建视图的功能

概念

下面会从SwiftUI的几个基础并且重要的知识概念出发,从而能够大致理清并了解SwiftUI 框架的大致组成结构,达到在使用SwiftUI这个框架时能够更加理性的目的

  • UI 控件
    • 常用控件
    • 控件样式
      • 链式调用
      • view modifier
    • 动画
  • 生命周期
    • 视图
    • 应用程序
  • 适配

UI 控件

SwiftUI中,有许多常用的UI 控件可用于构建应用程序的用户界面,同时还能对这些UI 控件的样式进行调整,设计出想要的页面效果

常用控件

  • Text:用于显示文本内容,支持自定义样式、字体和颜色
  • Label:用于显示带有图标的文本标签,通常用于表示标签或标题
  • Button:创建一个可点击的按钮,用于执行操作或导航到其他视图
  • LinkButton:类似于 Button,但用于打开外部链接或导航到其他应用程序
  • TextField:用于接受用户的文本输入
  • Image:用于显示图像,支持本地图像和网络图像
  • NavigationView:用于创建导航界面的容器
  • NavigationLink:创建一个可点击的导航链接
  • ScrollView:用于滚动内容,以显示更多的信息
  • List:用于显示可滚动的列表,支持分组和行操作
  • Form:类似于 List,但用于创建表单布局,自动适应键盘弹出
  • Alert:用于显示警报消息和操作
  • ActionSheet:用于显示一个带有多个操作选项的弹出菜单
  • VStack:垂直堆栈布局,用于按垂直方向排列视图
  • HStack:水平堆栈布局,用于按水平方向排列视图
提示
  • Section也是一个 UI 控件,但是不能单独使用,而是用于构建列表表格布局组件Section通常用于将列表表格中的内容分组显示
  • VStackHStack是用于页面布局的,经常频繁使用

控件样式

SwiftUI中构建用户界面时,可以使用链式调用封装 View Modifier来进行样式调整。这些方法使得在不同视图上应用相似的样式变得非常便捷,同时还可以增强代码的可读性和可维护性

1. 链式调用

SwiftUI中,可以通过链式调用在视图上应用多个修饰符,从而逐步构建视图的样式。每个修饰符调用都会返回一个新的视图,可以在其上继续应用其他修饰符

struct ProfileHeaderView: View {
    @State var lineWidth = CGFloat(4)
    @State var radius =  CGFloat(7)
    
    var body: some View {
        Image(avatarUrl)
            .resizable()
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.orange, lineWidth: lineWidth))
            .shadow(radius: radius)
            .animation(.default.repeatForever(), value: lineWidth)
            .animation(.default.repeatForever(), value: radius)
            .onAppear {
                lineWidth = 2
                radius = 4
            }
            .frame(width: 60, height: 60)
    }
}

上面这个例子是绘制一个包含外框且有动画的头像框,可以看到主要是使用Image这个控件来完成,外框、动画则都是对这个UI 控件的样式进行调整,采用链式调用内置的修饰符来完成

2. 封装 View Modifier

除了使用内置的修饰符外,您还可以创建自定义的View Modifier将常用的样式封装起来,以便在多个视图中重复使用,这使得你可以在代码中引入更高层次的抽象,使得代码更具可重用性可维护性

struct ProfileHeaderView: View {
    var body: some View {
        Image(avatarUrl)
            .resizable()
            .circle()
            .frame(width: 60, height: 60)
    }
}

struct CircleModifier: ViewModifier {
    @State var lineWidth = CGFloat(4)
    @State var radius =  CGFloat(7)
    
    func body(content: Content) -> some View {
        content
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.orange, lineWidth: lineWidth))
            .shadow(radius: radius)
            .animation(.default.repeatForever(), value: lineWidth)
            .animation(.default.repeatForever(), value: radius)
            .onAppear {
                lineWidth = 2
                radius = 4
            }
    }
}

extension Image {
    func circle() -> some View {
        modifier(CircleModifier())
    }
}

上面这个例子则是在链式调用的基础上进行了封装,把多个样式封装成一个样式行为,属于更高层次的样式使用。虽然实现的效果是一样的,但是当多处需要使用到上面的样式时,使用View Modifier封装的样式能让你少写更多相同样式代码

动画

SwiftUI中,可以使用内置的动画功能UI 控件添加动画效果,以增强用户体验。动画可以应用于属性的更改,从而使控件的状态转换更加平滑和引人注目

在上面的例子中有应用到动画修饰符,可以看到需要给动画定义一个触发时机

使用动画有两种方式

  • 动画修饰符.animation
  • 动画APIwithAnimation

下面就使用之前的例子对比演示一下两者使用上的一些区别:

@State var lineWidth = CGFloat(4)
@State var radius =  CGFloat(7)
    
// .animation
Image(avatarUrl)
    .animation(.default.repeatForever(), value: lineWidth)
    .animation(.default.repeatForever(), value: radius)
    .onAppear {
        lineWidth = 2
        radius = 4
    }
    
// withAnimation
Image(avatarUrl)
    .onAppear {
        withAnimation(.default.repeatForever()) {
            lineWidth = 2
            radius = 4
        }
    }

生命周期

SwiftUI的生命周期与UIkit差别挺大的,首先就是没有UIkit那么详细的生命周期函数,然后就是SwiftUI的生命周期分为视图应用程序两种

  • 视图:生命周期函数被缩减为只剩onAppearonDisappear两个
  • 应用程序:生命周期与ScenePhase密切相关
    • .active:应用程序处于活动状态,即在前台运行,用户可以与应用程序进行交互
    • .inactive:应用程序处于非活动状态,即在前台运行,但用户不能与应用程序交互
    • .background:应用程序处于后台运行状态

SwiftUI应用程序生命周期的使用案例:

struct MySwiftUIApp: App { 
    @Environment(\.scenePhase) private var scenePhase: ScenePhase
    
    var body: some Scene { 
        WindowGroup { 
            ContentView() 
        }
        .onChange(of: scenePhase) { newScenePhase in
            layoutWithScenePhase(newScenePhase: newScenePhase)
        }
        
        private func layoutWithScenePhase(newScenePhase: ScenePhase) {
            switch newScenePhase{
            case .active:
                // code
            case .inactive:
                // code
            case .background: 
                // code
            @unknown default: break
            }
        }
    }
}

适配

SwiftUI中,适配涉及确保应用程序在不同设备之间(如iPhoneiPad)以及不同屏幕尺寸分辨率之间都能正常显示

下面列举两个SwiftUI针对不同设备类型的适配方案:

  • Size Classes(尺寸类别)
  • UIDevice.current.userInterfaceIdiom

Size Classes

SwiftUI使用尺寸类别来表示设备的大小方向,如:compactregular。在视图中使用.horizontalSizeClass.verticalSizeClass来检测设备的尺寸类别,并根据需要应用不同的布局和样式

struct ContentView: View { 
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.verticalSizeClass) var verticalSizeClass 
    
    var body: some View { 
        VStack(alignment: .leading) { 
            if horizontalSizeClass == .compact && verticalSizeClass == .regular { 
                layoutWithUserInterfaceIdiom()
            } else { 
                HStack {
                    layoutWithUserInterfaceIdiom()
                }
            } 
        } 
    } 
}

UIDevice.current.userInterfaceIdiom

UIDevice.current.userInterfaceIdiomUIKit中用于判断设备类型的属性,可以在iPhoneiPad之间进行适配,但如果需要在SwiftUI中使用UIKit属性来判断设备类型,也是可以的

struct ProfileHeaderView: View {
    @State var userInterfaceIdiom = UIDevice.current.userInterfaceIdiom
    
    var body: some View {
        VStack(alignment: .leading) {       
            if userInterfaceIdiom == .phone {
                HStack {
                    layoutWithUserInterfaceIdiom()
                }
            } else {
                layoutWithUserInterfaceIdiom()
            }
        }
    }
}

小结

对于SwiftUI的学习,与UIkit的学习路线大致上是相似的,但由于SwiftUI是一种全新的声明式框架,使用方式和一些概念可能会有所不同

对于SwiftUI 框架的使用,本文主要讲述了其通用 UI控件样式动画生命周期以及适配问题等,在后续的文章中会陆续对SwiftUI中的属性包装器缓存以及网络请求进行谈论及总结