SwiftUI学习

学习文章
SwiftUI要求
  1. iOS13.0+
快捷键
  1. control + option + 点击:出现属性编辑器
  2. command + 点击:出现快捷菜单
  3. command + shift + LShow Library弹窗
布局
  1. VStack - 垂直布局
  2. HStack - 水平布局
  3. Spacer - 间距
  4. Text - 文本
  5. Image - 图片
  6. Divider - 分割线
  7. Group - 组
  8. ScrollView - 滚动视图
  9. Path - 路径
  10. Shaper - 形状
  11. FormSection - 表单
  12. Color.red - 填充颜色
  13. ForEach - 循环
  14. LinearGradient(线性渐变)、RadialGradient(径向渐变)、AngularGradient(角度渐变)
代码解析
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .font(.title)
            .foregroundColor(.yellow)
            .bold()
            .italic()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
  1. ContentView为布局,ContentView_Previews为预览布局
  2. body为计算属性,类型为不透明类型的View,不透明类型使用some修饰
  3. Swift语法,只有一行代码,return可以省略
  4. some修饰,表示不透明类型,总会返回某一个特定的类型,但是不知道是哪一种
  1. 可以返回关联类型的协议类型
  2. 安全性:不对外暴露具体的返回类型
  3. 用来解决SwiftUI繁琐的不确定返回类型问题
使用技巧
  1. 可以在右上角+里拖动空间到代码中

  2. 使用import导入所需的库

  3. 可以新建SwiftUI View

  4. ignoresSafeArea忽略safeArea的边距,用在feame

  5. 布局group组件可增加padding

  6. VStack可添加fontforegroundColor等属性,对所有包含的元素起效

  7. 串联属性,每一个点语法属性,返回当前对象

Text("Hello world!")
   .font(.title)
   .foregroundColor(.purple)
   
A modifier returns a view that applies a new behavior or visual change. You can chain multiple modifiers to achieve the effects you need.
  1. 使用previewLayout可以定义预览的窗口的大小,也可以使用Group同时预览多个窗口,通用属性可以提取到外面
struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}
  1. Identifiable:作为唯一标识
    遍历需要唯一标识来遍历,如下:
List(landmarks, id: \.id) { landmark in
    NavigationLink(
        destination: LandmarkDetail()) {
        LandmarkRow(landmark: landmark)
    }
}

如果让列表中元素遵守Identifiable协议,遍历处即可省略id参数,model中需要有名称为id的属性

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
  1. 页面跳转使用NavigationLinkdestination为要跳转的页面
NavigationLink(destination: LandmarkDetail()) {
    LandmarkRow(landmark: landmark)
}
  1. 使用NavigationView为页面田健导航栏,可设置 navigationTitle
NavigationView {
    List(landmarks) { landmark in
        NavigationLink(destination: LandmarkDetail()) {
            LandmarkRow(landmark: landmark)
        }
    }
    .navigationTitle("Landmarks")
}
  1. 预览窗口按钮作用:
    第一个按钮:Live PreviewDebug Preview,未打开只能查看页面,不能点击等,打开之后可以点击跳转页面等交互操作
    第二个按钮:Preview On Device,连上真机点击之后,预览可以同步在真机上展示
    第三个按钮:Inspect Preview,可以打开窗口属性窗口,可以设置预览窗口属性
    第四个按钮:Duplicate Preview ,可以复制创建多个预览窗口

  2. 代码控制预览的机型

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)"))
    }
}

// 多设备同时预览
struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone 8", "iPhone 12 Pro Max"], id: \.self) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
    }
}
  1. 组合静态的View和动态的viewList里,可使用List + ForEach
List(filteredLandmarks) { landmark in
    NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
        LandmarkRow(landmark: landmark)
    }
}

替换为

List {
    Toggle(isOn: $showFavoriteOnly) {
        Text("Favorites only")
    }
    ForEach(filteredLandmarks) { landmark in
        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
            LandmarkRow(landmark: landmark)
        }
    }
}

如果遍历的对象没有实现Identifiable协议,则需要传id

List {
    ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
        Text(key)
    }
}
  1. ObservableObject协议
    当遵守ObservableObject协议的数据更新时,绑定数据的view会自动更新
final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
}
  1. @Published
    使用@Published修饰监听对象的属性,表示该对象的属性需要把属性值的改变更新进去
final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
}
  1. @StateObject
    使用@StateObject初始化一个监听对象的数据,
    使用.environmentObject把数据设置到环境对象里,
    在需要的地方去取环境对象@EnvironmentObject var modelData: ModelData进行使用
@main
struct MySwiftUIApp: App {
    // 定义
    @StateObject private var modelData = ModelData()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
            // 设置
                .environmentObject(modelData)
        }
    }
}

// 取
@EnvironmentObject var modelData: ModelData

// 使用
modelData.landmarks
  1. Booltoggle()方法:在truefalse之前切换

  2. @EnvironmentObject属性用于在下级view中接收传递过来的参数

  3. environmentObject(_:)方法用于向下级传递参数

  4. @Binding:绑定修饰符用于修饰一个值,这个值用来改变另外一个值

绑定:
@Binding var isSet: Bool

修改:
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
  1. 定义有状态的字段,使用@State修饰,定义为private,并且赋初始值
    @State private var showFavoritesOnly = false

  2. @State
    使用@State属性为应用程序中的数据建立一个真实来源,您可以从多个视图中修改这些数据。SwiftUI管理底层存储并根据值自动更新视图。

  3. 使用Path直接绘制,可以当做View来使用

  4. View动画,包括颜色、透明度、旋转、大小和其他属性等,可以使用withAnimation来包裹状态State实现动画

withAnimation {
    self.showDetail.toggle()
}

withAnimation(.easeInOut(duration: 4)) {
    self.showDetail.toggle()
}
  1. 调用Viewtransition可以为View添加动画
    HikeDetail(hike: hike).transition(.slide)
    自定义transition可自定义动画
extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.slide
    }
}

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.move(edge: .trailing)
    }
}

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)
    }
}
  1. ListRowInsets用来调整一个viewlist中的上下左右间距

  2. CaseIterable,用在enum中用来获取allCases方法

  3. @EnvironmentSwiftUI提供了在环境中值的存储,使用@Environment可以访问值,并可以读写

@Environment(\.editMode) var editMode
  1. @State@ObservableObject@Binding@EnvironmentObject区别
    @State@ObservableObject之间有一些细微的差异。这些都是很重要的,因为它们都有不同的用途。
    @State在视图本地。值或数据在视图中本地保存。它由框架管理,由于它存储在本地,因此它是一个值类型。
    使用@State来存储不断变化的数据。记住,我们所有的SwiftUI视图都是结构体,这意味着没有@State之类的东西就无法更改它们。
    @ObservableObject在视图外部,并且不存储在视图中。它是一种引用类型,因为它不在本地存储,而只是具有对该值的引用。这不是由框架自动管理的,而是开发人员的责任。这最适用于外部数据,例如数据库或由代码管理的模型。
    @Binding也在视图内,但是与@State区别在于@Binding用于不通视图之间的参数传递。@Binding@ObservedObject一样都是传递引用。
    @EnvironmentObject 可以理解为全局变量

  2. ObservableObject@Published
    遵循 ObservableObject 协议的类可以使用 SwiftUI@Published 属性包装器来自动发布属性的变化,以便使用该类的实例的任何视图能够自动重新调用 body 属性,保持界面与数据的一致。
    @Published var profile = Profile.default
    界面中使用@Binding来绑定UI
    ``

  3. 使用UIViewRepresentable来将UIKit中已有的ViewSwiftUI中使用
    使用UIViewControllerRepresentableUIKit中的UIViewControllerSwiftUI中使用
    UIViewRepresentable
    使用方法如下:

import SwiftUI
import UIKit

struct PageControl: UIViewRepresentable {
    var numberOfPages: Int
    @Binding var currentPage: Int
    
    func makeUIView(context: Context) -> UIPageControl {
        let control = UIPageControl()
        control.numberOfPages = numberOfPages
        return control
    }
    
    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPage
    }
}
  1. SwiftUI画布的Resume快捷键:Option + Command + P

  2. Form表单、Section分段、Group分组
    SwiftUI限制SectionGroup包含不能超过10个,Section可设置headerfooter

Form {
            Section(header: Text("Section 1 header").bold(), footer: Text("Section 1 footer")) {
                Text("Placeholder 1")
                Text("Placeholder 2")
                Text("Placeholder 3")
                Group() {
                    Text("Placeholder 1")
                    Text("Placeholder 2")
                    Text("Placeholder 3")
                    Text("Placeholder 4")
                    Text("Placeholder 5")
                    Text("Placeholder 6")
                    Text("Placeholder 7")
                    Text("Placeholder 8")
                    Text("Placeholder 9")
                    Text("Placeholder 10")
                }
            }
            
            Group() {
                Text("Placeholder 1")
                Text("Placeholder 2")
            }
        }
WX20210127-160605@2x.png
  1. 添加导航栏使用navigationBarTitledisplayMode设置显示样式
NavigationView {
            Form {
                Section {
                    Text("Hello World")
                }
            }
            .navigationBarTitle("SwiftUI", displayMode: .inline)
        }
  1. 就像SwiftUI的其他视图一样,VStack最多可以有10个子节点——如果您想添加更多子节点,应该将它们包装在一个Group中。

  2. Color.red本身就是一个视图,这就是为什么它可以像形状和文本一样使用。它会自动占用所有可用空间。

  3. Color.primarySwiftUI中文本的默认颜色,根据用户的设备是在亮模式还是在暗模式下运行,它将是黑色还是白色。还有Color.secondary,它也可以是黑色或白色,这取决于设备,但现在它有轻微的透明度,以便后面的一点颜色可以穿透。

  4. 如果要将内容置于安全区域之下,可以使用edgesIgnoringSafeArea()修饰符指定要运行到的屏幕边缘。

Color.red.ignoresSafeArea()
Color.red.edgesIgnoringSafeArea(.all)
  1. 渐变色
VStack {
            // 线性渐变 LinearGradient 沿一个方向运行,因此我们为其提供了一个起点和终点
            LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .leading, endPoint: .trailing)
            // 径向渐变 RadialGradient 以圆形向外移动,因此,我们没有指定方向,而是指定了起点和终点半径——颜色应从圆心到圆心的距离开始和停止变化
            RadialGradient(gradient: Gradient(colors: [.blue, .black]), center: .center, startRadius: 20, endRadius: 200)
            // 角度渐变 AngularGradient,尽管您可能听说过其他地方将其称为圆锥形或圆锥形渐变。这使颜色围绕一个圆圈循环而不是向外辐射
            AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center)
        }
image.png
  1. 如果您发现图像已被某种颜色填充,例如显示为纯蓝色而不是实际图片,则可能是SwiftUI为它们着色以显示它们是可点击的。要解决此问题,请使用renderingMode(.original)修饰符强制SwiftUI显示原始图像,而不是重新着色的版本。
Image("Image Name")
                .renderingMode(.original)
  1. Alert的使用
struct ContentView: View {
    @State private var showAlert = false

    var body: some View {
        VStack {
            Button(action: {
                showAlert = true
            }) {
                Text("按钮")
            }
            .alert(isPresented: $showAlert, content: {
                Alert(title: Text("标题"), message: Text("文本内容"), primaryButton: .cancel {
                    print("点击取消")
                }, secondaryButton: .default(Text("确定")) {
                    print("点击确定")
                })
            })
        }
    }
}
image.png
  1. Swift内置形状:
    矩形Rectangle、圆角矩形RoundedRectangle、圆形Circle、胶囊Capsule和椭圆Ellipse
    使用方法:.clipShape(Capsule())

  2. 切割、描边、阴影

Image("xixi")
                // 边缘形状
                .clipShape(Circle())
                // 描边
                .overlay(Circle().stroke(Color.yellow, lineWidth: 2))
                // 阴影
                .shadow(color: .blue, radius: 20)
image.png
  1. SwiftUI为什么使用结构体而不是用类?

首先,有一个性能因素:结构体比类更简单,更快。我之所以说性能因素,是因为很多人认为这是SwiftUI使用结构体的主要原因,而实际上这只是更大范围的一部分。

UIKit中,每个视图都来自一个名为UIView的类,该类具有许多属性和方法:背景色,确定其放置方式的约束,用于将其内容呈现到其中的图层等等。其中有很多,每个UIViewUIView子类都必须具有它们,因为继承是这样工作的。

视图作为结构体还是有很多更重要的事情:它迫使我们考虑以一种干净的方式隔离状态。您会发现,类能够自由更改其值,这可能导致代码混乱。

通过生成不会随时间变化的视图,SwiftUI鼓励我们转向更具功能性的设计方法:在将数据转换为UI时,我们的视图变成简单的,惰性的东西,而不是会失去控制的智能化的东西。

当您查看可以作为视图的事物时,可以看到这一点。我们已经使用了Color.redLinearGradient作为视图——包含很少数据的简单类型。实际上,您不能找到比使用Color.red作为视图的更好的主意:除了“用红色填充我的空间”之外,它不包含任何信息。

相比之下,Apple的UIView文档列出了UIView拥有的约200种属性和方法,无论是否需要它们,所有这些属性和方法都将传递给其子类。

提示:如果您在视图中使用类,则可能会发现代码无法编译或在运行时崩溃。

  1. SwiftUI应用点语法修改视图,返回的也是视图类型。每次我们修改视图时,SwiftUI都会使用以下泛型来应用该修饰符:ModifiedContent<OurThing, OurModifier>

  2. 为什么SwiftUI使用 some View 作为视图类型?
    返回some View与仅返回View相比有两个重要区别:

  • 1、我们必须始终返回相同类型的视图。
  • 2、即使我们不知道返回的视图类型,编译器也同样不知道。
    这种代码是不允许的:
var body: some View {
    if self.useRedText {
        return Text("Hello World")
    } else {
        return Text("Hello World")
            .background(Color.red)
    }
}

some View意味着“将返回一种特定类型的视图,但我们不想说它是什么。”由于SwiftUI使用通用的ModifiedContent包装器创建新视图的方式, Text(…)Text(…).background(Color.red)是不同的底层类型,这与some View不兼容。

SwiftUI使用ModifiedContent构建数据的方式。
SwiftUI是如何处理VStack这样的东西的——它符合View协议,如果您创建了一个包含两个文本视图的VStack,那么SwiftUI会无声地创建一个TupleView来包含这两个视图。TupleView的一个版本可以跟踪十种不同的内容:

TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>

这就是为什么SwiftUI在一个父级中不允许超过10个视图的原因:他们编写了TupleView的版本,可以处理2到10个视图,但不能超过10个。

  1. 自定义修饰符,使用ViewModifier
import SwiftUI

struct MyViewModifier: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
                // 使用修饰符
                .modifyTitle()
            
            Text("Hello, World!")
                // 使用修饰符
                .modifySubTitle(text: "前缀")
        }
    }
}

struct MyViewModifier_Previews: PreviewProvider {
    static var previews: some View {
        MyViewModifier()
    }
}

// 自定义修饰符
struct Title: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.title)
            .foregroundColor(.white)
            .padding()
            .background(Color.red)
            .clipShape(RoundedRectangle(cornerRadius: 5.0))
    }
}

// 自定义修饰符,并重新构建视图
struct SubTitle: ViewModifier {
    var text: String
    func body(content: Content) -> some View {
        VStack {
            content
                .font(.subheadline)
                .foregroundColor(.gray)
                .padding()
                .background(Color.green)
                .clipShape(RoundedRectangle(cornerRadius: 5.0))
            Text(text)
                .font(.subheadline)
                .foregroundColor(.blue)
        }
    }
}

// 扩展修饰符
extension View {
    func modifyTitle() -> some View {
        self.modifier(Title())
    }
    
    func modifySubTitle(text: String) -> some View {
        self.modifier(SubTitle(text: text))
    }
}

  1. LazyVGridLazyHGrid使用(iOS14新增)
let text = (1 ... 10).map { "Hello\($0)" }
    // 以最小宽度160斤可能在一行放入grid
    let columns = [GridItem(.adaptive(minimum: 80))]
    // 每行三个grids,大小灵活分配
    let columnsFixed = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
    ]
    // 第一个100固定,第二个尽量填满
    let columnsFixed100 = [
        GridItem(.fixed(100)),
        GridItem(.flexible()),
    ]

    var rows: [GridItem] =
        Array(repeating: .init(.fixed(20)), count: 2)

    var body: some View {
        ScrollView {
            Section(header: Text("最小160")) {
                LazyVGrid(columns: columns, spacing: 20) {
                    ForEach(text, id: \.self) {
                        item in
                        Text(item)
                    }
                }
            }

            Section(header: Text("每行三个Grid")) {
                LazyVGrid(columns: columnsFixed, spacing: 20) {
                    ForEach(text, id: \.self) {
                        item in
                        Text(item)
                    }
                }
            }

            Section(header: Text("第一个固定100")) {
                LazyVGrid(columns: columnsFixed100, spacing: 20) {
                    ForEach(text, id: \.self) {
                        item in
                        Button(item) {
                            print("itme pressed")
                        }
                    }
                }
            }

            ScrollView(.horizontal) {
                LazyHGrid(rows: rows, alignment: .top) {
                    ForEach(0 ... 79, id: \.self) {
                        let codepoint = $0 + 0x1F600
                        let codepointString = String(format: "%02X", codepoint)
                        Text("\(codepointString)")
                            .font(.footnote)
                        let emoji = String(Character(UnicodeScalar(codepoint)!))
                        Text("\(emoji)")
                            .font(.largeTitle)
                    }
                }
            }
        }
    }
image.png
  1. ForEach使用区别:
let agents = ["Cyril", "Lana", "Pam", "Sterling"]
VStack {
    ForEach(0 ..< agents.count) {
        Text(self.agents[$0])
    }
}

我们回到Swift如何识别数组中的值。当我们使用0..<50..<agents.count这样的范围时,Swift确信每个项目都是唯一的,因为它将使用范围中的数字——每个数字在循环中只使用一次,所以它肯定是唯一的。
但是当使用字符串时,不会标识为唯一,导致body被调用时会被重建。因此可以使用id来标识,如下:

VStack {
    ForEach(agents, id: \.self) {
        Text($0)
    }
}

另外,为了标识视图的唯一,可以用Identifiable协议来实现:

定义:

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol Identifiable {

    /// A type representing the stable identity of the entity associated with
    /// an instance.
    associatedtype ID : Hashable

    /// The stable identity of the entity associated with this instance.
    var id: Self.ID { get }
}

使用:

struct ModelData: Identifiable {
    var id: Int
}
  1. 使用定制绑定Binding
    简单使用:
struct ContentView: View {
    @State private var selection = 0
    
    var body: some View {
        let binding = Binding(
            get: { self.selection },
            set: { self.selection = $0 }
        )
        
        return VStack {
            Picker("Select", selection: binding) {
                ForEach(0 ..< 3) {
                    Text("Item \($0)")
                }
            }
            .pickerStyle(SegmentedPickerStyle())
            
            Text("\(selection)")
        }
    }
}

因此,该绑定实际上只是作为一个传递——它本身不存储或计算任何数据,而是作为UI和被操纵的底层状态值之间的一个填充。

但是,请注意,选择器现在是使用selection:binding进行的,不需要美元符号。我们不需要在这里明确要求双向绑定,因为它已经是了。

高级使用:

struct ContentView: View {
    @State private var agreedToTerms = false
    @State private var agreedToPrivacyPolicy = false
    @State private var agreedToEmails = false
    
    var body: some View {
        let agreeToAll = Binding<Bool>(
            get: {
                self.agreedToTerms && self.agreedToPrivacyPolicy && self.agreedToEmails
            },
            set: {
                self.agreedToTerms = $0
                self.agreedToPrivacyPolicy = $0
                self.agreedToEmails = $0
            }
        )
        
        return VStack {
            Toggle(isOn: $agreedToTerms) {
                Text("agreedToTerms")
            }
            
            Toggle(isOn: $agreedToPrivacyPolicy) {
                Text("agreedToPrivacyPolicy")
            }
            
            Toggle(isOn: $agreedToEmails) {
                Text("agreedToEmails")
            }
            
            Toggle(isOn: agreeToAll) {
                Text("agreeToAll")
            }
        }
        .padding()
    }
}
image.png
  1. doubleString保留几位小数
// 保留2位小数
Text("\(sleepAmount, specifier: "%.2f") 小时")
 // 保留2位小数,并自动删除末尾不需要的0
Text("\(sleepAmount, specifier: "%.2g") 小时")
image.png
  1. DatePicker使用
struct DatePickerView: View {
    @State private var wakeUp = Date()
    var body: some View {
        VStack {
            // 有标题
            DatePicker("Please enter a date", selection: $wakeUp)
            // 无标题
            DatePicker("Please enter a date", selection: $wakeUp)
                .labelsHidden()
            // 无标题,有时间范围
            DatePicker("Please", selection: $wakeUp, in: Date() ... Date().addingTimeInterval(86400))
                .labelsHidden()
            DatePicker("Please", selection: $wakeUp, in: Date()...)
                .labelsHidden()
        }
    }
}
image.png
  1. DateComponentsDateFormatter使用
        // hour、minute通过DateComponents生成Date
        var dateComponents = DateComponents()
        dateComponents.hour = 8
        let date = Calendar.current.date(from: dateComponents)
        
        // Date通过DateComonents获取hour、minute
        let someDate = Date()
        let components = Calendar.current.dateComponents([.hour, .minute], from: someDate)
        let hour = components.hour ?? 0
        let minute = components.minute ?? 0
        
        // Date转String
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .short
        let dateString = dateFormatter.string(from: Date())
  1. SwiftUI读取本地项目文件
if let startWordsUrl = Bundle.main.url(forResource: "start", withExtension: "txt") {
            if let startWords = try? String(contentsOf: startWordsUrl) {
                let allWords = startWords.components(separatedBy: "\n")
                rootWord = allWords.randomElement() ?? "silkworm"
                return
            }
        }
  1. 显示视图时调用的闭包onAppear
        NavigationView {
            VStack {
                TextField("输入单词", text: $newWord, onCommit: addNewWord)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                    .autocapitalization(.none)

                List(usedWords, id: \.self) {
                    Image(systemName: "\($0.count).circle")
                    Text($0)
                }
            }
            .navigationTitle(rootWord)
            .onAppear(perform: startGame)
        }
  1. 使用多个.animation对不同的动画,进行分别处理
struct ContentView: View {
    @State private var enabled = false
    
    var body: some View {
        VStack {
            Button("Tap Me") {
                self.enabled.toggle()
            }
            .frame(width: 200, height: 200)
            .background(enabled ? Color.blue : Color.red)
            .animation(.default) // 针对颜色的动画
            .foregroundColor(.white)
            .clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
            .animation(.interpolatingSpring(stiffness: 10, damping: 1)) // 针对形状的动画
        }
    }
}

禁用动画:.animation(nil)

  1. 手势动画
struct ContentView: View {
    @State private var dragAmount = CGSize.zero

    var body: some View {
        LinearGradient(gradient: Gradient(colors: [.yellow, .red]), startPoint: .topLeading, endPoint: .bottomTrailing)
            .frame(width: 300, height: 200)
            .clipShape(RoundedRectangle(cornerRadius: 10))
            // 视图位置改变
            .offset(dragAmount)
            // 添加拖动手势
            .gesture(
                DragGesture()
                    .onChanged {
                        // 实时根据手势位置改变视图的位置
                        self.dragAmount = $0.translation
                    }
                    .onEnded { _ in
                        // 弹性动画归0
                        withAnimation(.spring()) {
                            self.dragAmount = .zero
                        }
                    }
            )
    }
}
  1. 结构体和类的使用区别
  • 结构体一直拥有唯一的所有者,而对于类,多个对象可以指向同一个值。
  • 类不需要在更改其属性的方法之前使用mutating关键字,因为您可以更改常量类的属性。

实际上,这意味着,如果我们有两个SwiftUI视图,并且我们将同一个结构体发送给它们,那么它们实际上都有该结构体的唯一副本;如果其中一个更改了它,那么另一个将看不到该更改。另一方面,如果我们创建一个类的实例并将其发送到两个视图,它们将共享更改。

对于SwiftUI开发人员来说,这意味着如果我们想要在多个视图之间共享数据——如果我们想要两个或多个视图指向同一个数据,以便在一个更改时它们都得到这些更改——我们需要使用类而不是结构体。

  1. 为什么使用@ObservedObject
    如果使用类的同时,使用@State来让SwiftUI监听值的改变,虽然值改变了,但是SwiftUI监听不到类中值的改变,不会对body进行销毁和重建,所以需要使用@OvervedObject来处理该问题。

当我们使用@State时,我们要求SwiftUI监视属性的更改。因此,如果我们更改字符串、翻转布尔值、添加到数组等,则属性已更改,SwiftUI将重新调用视图的body属性。

User是一个结构体时,每次我们修改该结构体的属性时,Swift实际上是在创建该结构的新实例。@State能够发现这个变化,并自动重新加载我们的视图。现在我们有了一个类,这种行为不再发生:Swift可以直接修改值。

还记得我们如何为修改属性的结构体方法使用mutating关键字吗?这是因为,如果我们将结构体的属性创建为变量,但结构体本身是常量,则无法更改属性——Swift需要能够在属性更改时销毁和重新创建整个结构体,而常量结构则不可能这样做。类不需要mutating关键字,因为即使类实例标记为常量Swift,仍然可以修改变量属性。

我知道这听起来非常理论化,但这里有个问题:既然User是一个类,那么属性本身不会改变,所以@State不会注意到任何事情,也无法重新加载视图。是的,类中的值正在更改,但是@State不监视这些值,所以实际上发生的情况是,类中的值正在更改,但视图不会重新加载以反映该更改。

为了解决这个问题,是时候抛弃@State了。相反,我们需要一个更强大的属性包装器,名为@ObservedObject

  1. @ObservedObject@Published的使用
    如果需要在多个视图之间共享数据的话,可使用@ObservedObject@EnvironmentObject

@Published用于类中,通知关注类的所有视图在类发生改变时,去重新加载。

class User {
    // @Published通知关注类的所有视图在类发生改变时,去重新加载
    @Published var firstName = "zhiying"
    @Published var lastName = "yuan"
}

@ObservedObject用于获知知道哪些类在改变时能通知视图,它告诉SwiftUI监视类中的任何的更改公告。

@ObservedObject属性包装器只能用于符合ObservableObject协议的类型。该协议没有任何要求,实际上意味着“我们希望其他事物能够监视此更改”。

// 类遵守ObservableObject协议
class User: ObservableObject {
    // @Published 通知关注类的所有视图在类发生改变时,去重新加载
    @Published var firstName = "zhiying"
    @Published var lastName = "yuan"
}

struct ContentView: View {
    // @ObservedObject 用于标记哪些类在改变时通知视图加载视图
    @ObservedObject var user = User()
    
    var body: some View {
        VStack {
            Text("名字是\(user.firstName)\(user.lastName)")
            
            TextField("firstName", text: $user.firstName)
            TextField("lastName", text: $user.lastName)
        }
    }
}

三个步骤:

  • 创建一个符合ObservableObject协议的类。
  • @Published标记一些属性,以便使用该类的所有视图在更改时都得到更新。
  • 使用@ObservedObject属性包装器创建我们的类的实例。
  1. 弹出模态视图,并通过获取全局变量来关闭模态视图

弹出

struct ContentView: View {
    @State private var showSheet = false
    
    var body: some View {
        VStack {
            Button("show sheet") {
                self.showSheet.toggle()
            }
            .sheet(isPresented: $showSheet, content: {
                SecondView()
            })
        }
    }
}

关闭

struct SecondView: View {
    // 获取全局环境变量 presentationMode
    @Environment(\.presentationMode) var secondPresentationMode

    var body: some View {
        Button("关闭") {
            // 通过获取到的全局环境变量,来关闭模态视图
            self.secondPresentationMode.wrappedValue.dismiss()
        }
    }
}
  1. SwiftUI\.self是什么?
    [SwiftUI 100天] Core Data ForEach .self 的工作机制
struct ContentView: View {
    @State private var numbers = [Int]()
    
    var body: some View {
        VStack {
            List {
                ForEach(numbers, id: \.self) {
                    Text("\($0)")
                }
            }
        }
    }
}

之前我们了解了使用ForEach来创建动态视图的不同方式,它们都有一个共同点:SwiftUI 需要知道如何唯一识别动态视图的每一项,以便正确地动画化改变。

如果一个对象遵循Identifiable协议,那么 SwiftUI 会自动使用它的id属性作为唯一标识。如果我们不使用Identifiable,那就需要指定一个我们知道是唯一的属性的 key path,比如图书的 ISBN 号。但假如我们不遵循Identifiable也没有唯一的 key path,我们通常会使用.self。

我们对原始数据类型,例如Int和String使用过.self,就像下面这样:

List {
ForEach([2, 4, 6, 8, 10], id: .self) {
Text("($0) is even")
}
}
对于 Core Data 为我们生成托管类,我们也使用.self,当时我没有解释这是如何工作的,或者说它究竟是如何与我们的ForEach关联的。不过今天我要再来讨论这个问题,因为我觉得这会给你提供一些有助益的洞察。

当我们使用.self作为标识符时,我们指的是“整个对象”,但实践上这个指代并没有包含太多的含义 —— 一个结构体就是结构体,除了内容之外,它并没有包含任何特定的标识信息。因此,实际发生的事情是,Swift 计算了结构体的哈希值 —— 一种以固定长度的值表示复杂数据的方法 —— 然后以哈希值作为标识符。

哈希值可以以很多种方法生成,但所有方法的背后的概念是一致的:

无论输入的数据多大,输出总是固定大小。
对同一个对象计算两次哈希值,应该返回相同的值。
这两点听起来很简单,但你思考一下:假设我们获取 “Hello World” 的哈希值和莎士比亚全集的哈希值,两者都将是相同的大小。这意味着我们是无法从哈希值还原出原始数据的 —— 我们无法从 40 个看起来完全随机的十六进制数字转换出莎士比亚全集。

哈希常见于数据校验。举个例子,假如你下载了一个 8GB 的 zip 文件,你可以通过对比你本地的哈希值和服务器上的哈希值来确认文件是正确的 —— 如果两者匹配,说明 zip 文件是一致的。哈希还被用于字典的键和值,这是为什么字典查询速度很快的原因。

上面说的这些很重要,因为 Xcode 为我们生成托管对象的类时,它会让这些类遵循Hashable,这是一个表示 Swift 可以从中生成哈希值的协议,也就是说,我们可以用它的.self作为标识符。这也是为什么String和Int可以用.self的原因:它们也遵循Hashable。

Hashable有点类似Codable:如果我们想让一个自定义类型遵循Hashable,那么只要它包含的所有东西也遵循Hashable,那我们就不必做额外的工作。为了说明这一点,我们可以创建一个自定义结构体,让它遵循Hashable而不是Identifiable,然后使用.self来标识它:

struct Student: Hashable {
let name: String
}

struct ContentView: View {
let students = [Student(name: "Harry Potter"), Student(name: "Hermione Granger")]

var body: some View {
    List(students, id: \.self) { student in
        Text(student.name)
    }
}

}
我们可以让Student遵循Hashable,因为它所有的属性都已经遵循Hashable,因此 Swift 会计算每个属性的哈希值,然后结合这些值产生一个代表整个对象的哈希值。当然,如果我们遇到两个同名的学生,那我们可能会遇到问题,这就像我们拥有一个包含两个相同字符串的字符串数组一样。

现在,你可能想,这样会导致问题吧:如果我们用相同的值创建了两个 Core Data 对象,它们会生成相同的哈希值,这样我们就遇到问题了。不过,其实 Core Data 是一种很聪明的方式来工作:它为我们创建的对象实际上有一组我们定义数据模型时定义的属性之外的其他属性,包括一个叫 ID 的对象 —— 这是一个可以唯一标识对象的标识符,不管对象包含的属性是什么。这些 ID 类似于 UUID,在我们创建对象时,Core Data 顺序产生它们。

因此,.self适用于所有遵循Hashable的类,因为 Swift 会为其生成哈希值并用该值作为对象的唯一标识。这对于 Core Data 的对象同样适用,因为它们已经遵循了Hashable。

警告: 虽然给一个对象计算两次哈希值应该返回相同的值,但在两次应用运行期间计算它们 —— 比如说,计算哈希值,退出应用,重启,然后再计算哈希值 —— 是有可能返回不同的值的。

  1. onDelete()的使用
  • 单个左滑删除
struct ContentView: View {
    @State private var numbers = [Int]()
    @State private var currentNumber = 1
    
    var body: some View {
        VStack {
            List {
                ForEach(numbers, id: \.self) {
                    Text("\($0)")
                }
                // onDelete只能添加在ForEach上
                .onDelete(perform: { indexSet in
                    // ForEach是由numbers数组创建的,可以直接将索引集直接传给numbers数组
                    numbers.remove(atOffsets: indexSet)
                })
            }
            
            Button("添加") {
                numbers.append(currentNumber)
                currentNumber += 1
            }
        }
    }
}
image.png
  • 多个点击删除
struct ContentView: View {
    @State private var numbers = [Int]()
    @State private var currentNumber = 1
    
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(numbers, id: \.self) {
                        Text("\($0)")
                    }
                    .onDelete(perform: { indexSet in
                        numbers.remove(atOffsets: indexSet)
                    })
                }
                
                Button("添加") {
                    numbers.append(currentNumber)
                    currentNumber += 1
                }
            }
            .navigationBarItems(leading: EditButton())
        }
    }
}
image.png
  1. 本地数据存储UserDefaults
存
UserDefaults.standard.setValue(self.tapCount, forKey: "tapCount")
取(未设置有默认值)
UserDefaults.standard.integer(forKey: "tapCount")
  1. 调整图片大小,以适应屏幕
struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                // GeometryReader 确保图像填充其容器视图的整个宽度
                GeometryReader(content: { geometry in
                    Image("WX20210226-120815")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: geometry.size.width)
                })

                VStack {
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                }
            }
            .navigationTitle(Text("我是标题"))
        }
    }
}
image.png
struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                // GeometryReader 确保图像填充其容器视图的整个宽度
                GeometryReader(content: { geometry in
                    Image("WX20210226-120815")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: geometry.size.width)
                })
                
                VStack {
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                }
                VStack {
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                }
                VStack {
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                }
                VStack {
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                }
                VStack {
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                    Text("哈哈哈哈哈")
                }
            }
            .navigationTitle(Text("我是标题"))
        }
    }
}
image.png
  1. List + ForEachScrollView + ForEach区别
    List + ForEach会在可见的时候才创建
    ScrollView + ForEach会一次性创建所有视图

List + ForEach

struct ContentView: View {
    var body: some View {
        NavigationView {
            // List + ForEach 会在可见的时候才创建
            List {
                ForEach(0..<100) {
                    CustomText("\($0) _")
                }
            }
            .navigationTitle(Text("标题"))
        }
    }
}

ScrollView + ForEach

struct ContentView: View {
    var body: some View {
        NavigationView {
            ScrollView {
                ForEach(0..<100) {
                    CustomText("\($0) _")
                }
                .frame(maxWidth: .infinity)
            }
            .navigationTitle(Text("标题"))
        }
    }
}
  1. 布局优先级layoutPriority
    所有视图的默认优先级均为0
struct ContentView: View {
    var body: some View {
        HStack(spacing: 16) {
            Text("Hello")
            Text("World")
            // 布局优先级layoutPriority,所有视图的默认优先级均为0
            Text("哈哈哈哈哈哈哈")
                .layoutPriority(1)
        }
        .font(.largeTitle)
        .lineLimit(1)
    }
}
image.png
  1. Path绘制线
struct ContentView: View {
    var body: some View {
        Path({ path in
            path.move(to: CGPoint(x: 200, y: 100))
            path.addLine(to: CGPoint(x: 100, y: 300))
            path.addLine(to: CGPoint(x: 300, y: 300))
            path.addLine(to: CGPoint(x: 200, y: 100))
        })
        // style - StrokeStyle用来控制每条线的连接方式
        .stroke(Color.blue.opacity(0.5), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round, miterLimit: 20, dash: [15], dashPhase: 55))
    }
}
image.png
  1. stride使用
    从起始值以指定值步幅到结束值的序列
从0度到360度,每22.5度一步生成一个序列
stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 8)
  1. stroke绘制边框
Circle()
                .stroke(Color.blue, lineWidth: 4)
                .padding(100)
image.png
  1. 循环绘制形状
struct ContentView: View {
    @State private var petalOffset = -20.0
    @State private var petalWidth = 100.0

    var body: some View {
        VStack {
            Flower(petalOffset: petalOffset, petalWidth: petalWidth)
                
                .fill(
                    // 填充渐变色
                    AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center),
                    // eoFill: 奇偶填充
                    style: FillStyle(eoFill: true)
                )


            Text("Offset")
            Slider(value: $petalOffset, in: -40 ... 40)
                .padding([.horizontal, .bottom])

            Text("Width")
            Slider(value: $petalWidth, in: 0 ... 100)
                .padding(.horizontal)
        }
        .padding(20)
    }
}

struct Flower: Shape {
    // 花瓣移离中心多少距离
    var petalOffset: Double = -20

    // 每片花瓣的宽度
    var petalWidth: Double = 100

    func path(in rect: CGRect) -> Path {
        // 容纳所有花瓣的路径
        var path = Path()

        // 从0向上计数到 pi * 2,每次移动 pi / 8
        for number in stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 8) {
            // 根据循环旋转当前的花瓣
            let rotation = CGAffineTransform(rotationAngle: number)

            // 将花瓣移到我们视野的中心
            let position = rotation.concatenating(CGAffineTransform(translationX: rect.width / 2, y: rect.height / 2))

            // 使用我们的属性以及固定的Y和高度为该花瓣创建路径
            let originalPetal = Path(ellipseIn: CGRect(x: CGFloat(petalOffset), y: 0, width: CGFloat(petalWidth), height: rect.width / 2))

            // 将我们的旋转/位置变换应用于花瓣
            let rotatedPetal = originalPetal.applying(position)

            // 将其添加到我们的主路径
            path.addPath(rotatedPetal)
        }

        // 现在将主径 return
        return path
    }
}
image.png
  1. ImagePaint 制作边框和填充
struct ContentView: View {
    var body: some View {
        VStack {
// sourceRect 相对大小和位置的CGRect 0表示“开始”,1表示“结束”
// scale 使用比例尺绘制示例图像,0.2表示该图像的显示尺寸为正常尺寸的1/5
            Text("1111")
                .frame(width: 180, height: 180, alignment: .center)
                .border(ImagePaint(image: Image("WX20210310-163132"), sourceRect: CGRect(x: 0, y: 0.25, width: 1, height: 0.5), scale: 0.1), width: 20)
        }
    }
}
image.png
  1. 启用Metal高性能渲染
    SwiftUI默认使用CoreAnimation来进行渲染,但是遇到复杂的渲染,可以启用高性能渲染Metal。
struct ContentView: View {
    @State private var colorCycle = 0.0
    
    var body: some View {
        VStack {
            ColorCyclingCircle(amount: self.colorCycle)
                .frame(width: 300, height: 300)
            Slider(value: $colorCycle)
        }
    }
}

struct ColorCyclingCircle: View {
    var amount = 0.0
    var steps = 100

    var body: some View {
        ZStack {
            ForEach(0..<steps) { value in
                Circle()
                    .inset(by: CGFloat(value))
                    .strokeBorder(LinearGradient(gradient: Gradient(colors: [
                        self.color(for: value, brightness: 1),
                        self.color(for: value, brightness: 0.5)
                    ]), startPoint: .top, endPoint: .bottom), lineWidth: 2)
            }
        }
        .drawingGroup()
    }

    func color(for value: Int, brightness: Double) -> Color {
        var targetHue = Double(value) / Double(self.steps) + self.amount

        if targetHue > 1 {
            targetHue -= 1
        }

        return Color(hue: targetHue, saturation: 1, brightness: brightness)
    }
}
image.png

通过应用一个称为drawingGroup()的新修改器来解决此问题。这告诉SwiftUI,在将视图内容作为单个呈现的输出放回到屏幕上之前,应将视图的内容呈现到屏幕外的图像中(离屏渲染),这要快得多。在幕后,该功能由Metal提供支持,MetalApple的框架,可直接与GPU协同工作以实现极快的图形。

重要提示:drawingGroup()修饰符有助于了解和保留您的武器库,这是解决性能问题的一种方法,但是您不应该经常使用它。添加屏幕外渲染过程可能会降低SwiftUI进行简单绘图的速度,因此,在尝试引入drawingGroup()之前,应等到遇到实际性能问题后再进行操作。

74、实现实施模糊、混合模式、饱和度调整等效果
SwiftUI使我们能够出色地控制视图的呈现方式,包括应用实时模糊,混合模式,饱和度调整等功能。

混合模式使我们可以控制一个视图在另一个视图上的渲染方式。默认模式是.normal,它只是将新视图中的像素绘制到后面的任何东西上,但是有很多选项可以控制颜色和不透明度。

struct ContentView: View {
    @State private var colorCycle = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                Image("demo1")
                
                Rectangle()
                    .fill(Color.blue)
                    // blendMode图像混合模式 默认normal
                    .blendMode(.softLight)
                    .frame(width: 500, height: 500, alignment: .center)
            }
            
            Image("demo1")
                .colorMultiply(.yellow)
        }
    }
}
image.png

之所以命名为“Multiply”,是因为它将每个源像素颜色与目标像素颜色相乘——在我们的示例中,是图像的每个像素和顶部的矩形的每个像素。每个像素具有RGBA的颜色值,范围从0(没有该颜色)到1(所有颜色),因此所得的最高颜色为1x1,最低的颜色为0x0。

对纯色使用乘法会产生一种非常常见的色调效果:黑色保持黑色(因为它们的颜色值为0,所以无论您将顶部乘以0都将产生0),而较浅的颜色会变成各种阴影着色。

混合模式screen,它的作用与乘法相反:将颜色反转,执行乘法,然后再次反转颜色,从而产生较亮的图像而不是较暗的图像。

常用用法:.colorMultiply(Color.red)

struct ContentView: View {
    @State private var amount: CGFloat = 0.0

    var body: some View {
        VStack {
            ZStack {
                Circle()
//                    .fill(Color.red)
                    .fill(Color(red: 1, green: 0, blue: 0))
                    .frame(width: 200 * amount)
                    .offset(x: -50, y: -80)
                    .blendMode(.screen)

                Circle()
//                    .fill(Color.green)
                    .fill(Color(red: 0, green: 1, blue: 0))
                    .frame(width: 200 * amount)
                    .offset(x: 50, y: -80)
                    .blendMode(.screen)

                Circle()
//                    .fill(Color.blue)
                    .fill(Color(red: 0, green: 0, blue: 1))
                    .frame(width: 200 * amount)
                    .blendMode(.screen)
            }
            .frame(width: 300, height: 300)

            Slider(value: $amount)
                .padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.black)
        .edgesIgnoringSafeArea(.all)
    }
}
image.png

模糊效果

struct ContentView: View {
    @State private var amount: CGFloat = 0.0

    var body: some View {
        VStack {
            // 模糊效果
            Image("demo1")
                .resizable()
                .scaledToFit()
                .frame(width: 200, height: 200)
                .saturation(Double(amount)) // 饱和度,用于调整颜色的数量
                .blur(radius: (1 - amount) * 20)
            
            Slider(value: $amount)
                            .padding()
        }
    }
}
image.png

75、edgesIgnoringSafeArea边界忽略safeArea安全区域

76、Shape形状设置动画(单个动画变量)

struct ContentView: View {
    @State private var insetAmount: CGFloat = 50
    
    @State private var rows = 4
    @State private var columns = 4

    var body: some View {
        Trapezoid(insetAmount: insetAmount)
                    .frame(width: 200, height: 100)
                    .onTapGesture {
                        // 添加动画
                        withAnimation(.linear(duration: 1)) {
                            self.insetAmount = CGFloat.random(in: 10...90)
                        }
                    }
    }
}

struct Trapezoid: Shape {
    var insetAmount: CGFloat
    
    // 使用 animatableData 给形状设置动画
    var animatableData: CGFloat {
        get { insetAmount }
        set { self.insetAmount = newValue }
    }

    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.move(to: CGPoint(x: 0, y: rect.maxY))
        path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: 0, y: rect.maxY))

        return path
   }
}

这里发生的事情非常复杂:当我们使用withAnimation()时,SwiftUI会立即将状态属性更改为其新值,但在幕后,随着动画的进行,它还在跟踪随时间的值变化。随着动画的进行,SwiftUI会将 Shape 的animatableData属性设置为最新值,这取决于我们来决定这意味着什么——在本例中,我们将其直接分配给insetAmount,因为这就是我们要进行动画处理的东西。

记住,SwiftUI在应用动画之前先评估视图状态,然后再应用动画。可以看到我们最初有评估为Trapezoid(insetAmount:50)的代码,但是在选择了一个随机数之后,我们最终得到了(例如)Trapezoid(insetAmount:62)。因此,它将在动画的整个长度内插值50到62,每次将形状的animatableData属性设置为最新的插值:51、52、53,依此类推,直到达到62。

77、Shape形状设置动画(多个动画变量)

struct ContentView: View {
    @State private var rows = 4
    @State private var columns = 4

    var body: some View {
        Checkerboard(rows: rows, columns: columns)
            .onTapGesture {
                // 添加动画
                withAnimation(.linear(duration: 1)) {
                    self.rows = 8
                    self.columns = 16
                }
            }
    }
}

struct Checkerboard: Shape {
    var rows: Int
    var columns: Int
    
    public var animatableData: AnimatablePair<Double, Double> {
        get {
           AnimatablePair(Double(rows), Double(columns))
        }

        set {
            self.rows = Int(newValue.first)
            self.columns = Int(newValue.second)
        }
    }

    func path(in rect: CGRect) -> Path {
        var path = Path()

        // 计算每行/列需要多大
        let rowSize = rect.height / CGFloat(rows)
        let columnSize = rect.width / CGFloat(columns)

        // 循环遍历所有行和列,从而使交替的正方形变为彩色
        for row in 0..<rows {
            for column in 0..<columns {
                if (row + column).isMultiple(of: 2) {
                    // 这个正方形应该是彩色的;在此处添加一个矩形
                    let startX = columnSize * CGFloat(column)
                    let startY = rowSize * CGFloat(row)

                    let rect = CGRect(x: startX, y: startY, width: columnSize, height: rowSize)
                    path.addRect(rect)
                }
            }
        }

        return path
    }
}

78、为@Published包装器添加Codable支持
使用:

import Foundation

class User: ObservableObject, Codable {
    @Published var name = "xixi"
    
    enum CodingKeys: CodingKey {
        case name
    }
    
    required init(from decoder: Decoder) throws {
        // decoder包含了所有的数据
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
    }
}

原因:使用@Published包装后的属性,被包装在了另外一个类型中,这个类型包含一些其他的功能。比如Published<String>,是一个包含字符串的可发布的对象。

79、使用URLSessionURLRequest请求数据

struct ContentView: View {
    @State private var results = [Result]()

    var body: some View {
        List(results, id: \.trackId) { item in
            VStack(alignment: .leading) {
                Text(item.trackName)
                    .font(.headline)
                Text(item.collectionName)
            }
        }
        .onAppear(perform: loadData)
    }
    
    func loadData() {
        guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
            print("Invalid URL")
            return
        }
        
        let request = URLRequest(url: url)
        
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let data = data {
                if let responseData = try? JSONDecoder().decode(Response.self, from: data) {
                    DispatchQueue.main.async {
                        self.results = responseData.results
                    }
                }
            }
        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct Response: Codable {
    var results: [Result]
}

struct Result: Codable {
    var trackId: Int
    var trackName: String
    var collectionName: String
}

80、disabled来控制控件是否可用

struct ContentView: View {
    @State private var username = ""
    @State private var email = ""

    var body: some View {
        Form {
            Section {
                TextField("Username", text: $username)
                TextField("Email", text: $email)
            }
            Section {
                Button("Create account") {
                    print("Creating account…")
                }.disabled(username.isEmpty || email.isEmpty)
            }
        }
    }
}
image.png

81、使用@Binding创建自定义视图,实现双向绑定

struct PushButton: View {
    let title: String
    @Binding var isOn: Bool
    
    var onColors = [Color.red, Color.yellow]
    var offColors = [Color(white: 0.6), Color(white: 0.4)]
    
    var body: some View {
        Button(title) {
            self.isOn.toggle()
        }
        .padding()
        .background(LinearGradient(gradient: Gradient(colors: isOn ? onColors : offColors), startPoint: .leading, endPoint: .trailing))
        .foregroundColor(.white)
        .clipShape(Capsule())
        .shadow(radius: 10)
    }
}
struct ContentView: View {
    @State private var rememberMe = false
    var body: some View {
        NavigationView {
            List {
                PushButton(title: "Remember Me", isOn: $rememberMe)
                Text(rememberMe ? "开启": "关闭")
            }
            .navigationBarTitle("Cupcake Corner")
        }
    }
}

如果不使用@Binding,外部页面中,使用外部页面的属性创建自定义页面,只是传入自定义页面参数,传入后,里面值的改变并不会传递到外面。

自定义页面中,将需要绑定的属性使用@Binding修饰符,绑定外部页面的属性,将自定义页面中的值的改变传递到外部页面中,同步改变。

82、使用CoreData来增删改查数据
相关文章:SwiftUI CoreData入门概念和基础大全

创建持久化控制器单例PersistenceController,并创建初始化持久化容器NSPersistentContainer
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CoreDataSwiftUIDemo")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                /*
                Typical reasons for an error here include:
                * The parent directory does not exist, cannot be created, or disallows writing.
                * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                * The device is out of space.
                * The store could not be migrated to the current model version.
                Check the error message to determine what the actual problem was.
                */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}
在app中创建全局持久化控制器,并将持久化控制器的持久化容器的上下文注入全局环境变量中
import SwiftUI

@main
struct CoreDataSwiftUIDemoApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}
创建项目同名的DataModel文件,后缀为.xcdatamodelId,创建entity
image.png
从全局环境变量中取出上下文

@Environment(\.managedObjectContext) private var viewContext

使用@FetchRequest装饰器,从数据库中读取指定entity的数据列表
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
往coredata添加数据,通过从全局环境变量中获取到的上下文,创建对象
    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
删除coredata数据
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容