SwiftUI學(xué)習(xí)

學(xué)習(xí)文章
SwiftUI要求
  1. iOS13.0+
快捷鍵
  1. control + option + 點擊:出現(xiàn)屬性編輯器
  2. command + 點擊:出現(xiàn)快捷菜單
  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. Form劈彪、Section - 表單
  12. Color.red - 填充顏色
  13. ForEach - 循環(huán)
  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為預(yù)覽布局
  2. body為計算屬性忌穿,類型為不透明類型的View束莫,不透明類型使用some修飾
  3. Swift語法碧注,只有一行代碼竖哩,return可以省略
  4. some修飾嘶窄,表示不透明類型怀跛,總會返回某一個特定的類型,但是不知道是哪一種
  1. 可以返回關(guān)聯(lián)類型的協(xié)議類型
  2. 安全性:不對外暴露具體的返回類型
  3. 用來解決SwiftUI繁瑣的不確定返回類型問題
使用技巧
  1. 可以在右上角+里拖動空間到代碼中

  2. 使用import導(dǎo)入所需的庫

  3. 可以新建SwiftUI View

  4. ignoresSafeArea忽略safeArea的邊距柄冲,用在feame

  5. 布局group組件可增加padding

  6. VStack可添加font吻谋、foregroundColor等屬性,對所有包含的元素起效

  7. 串聯(lián)屬性现横,每一個點語法屬性漓拾,返回當(dāng)前對象

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可以定義預(yù)覽的窗口的大小,也可以使用Group同時預(yù)覽多個窗口戒祠,通用屬性可以提取到外面
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:作為唯一標(biāo)識
    遍歷需要唯一標(biāo)識來遍歷骇两,如下:
List(landmarks, id: \.id) { landmark in
    NavigationLink(
        destination: LandmarkDetail()) {
        LandmarkRow(landmark: landmark)
    }
}

如果讓列表中元素遵守Identifiable協(xié)議,遍歷處即可省略id參數(shù)姜盈,model中需要有名稱為id的屬性

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
  1. 頁面跳轉(zhuǎn)使用NavigationLink低千,destination為要跳轉(zhuǎn)的頁面
NavigationLink(destination: LandmarkDetail()) {
    LandmarkRow(landmark: landmark)
}
  1. 使用NavigationView為頁面田健導(dǎo)航欄,可設(shè)置 navigationTitle
NavigationView {
    List(landmarks) { landmark in
        NavigationLink(destination: LandmarkDetail()) {
            LandmarkRow(landmark: landmark)
        }
    }
    .navigationTitle("Landmarks")
}
  1. 預(yù)覽窗口按鈕作用:
    第一個按鈕:Live PreviewDebug Preview馏颂,未打開只能查看頁面示血,不能點擊等棋傍,打開之后可以點擊跳轉(zhuǎn)頁面等交互操作
    第二個按鈕:Preview On Device,連上真機點擊之后难审,預(yù)覽可以同步在真機上展示
    第三個按鈕:Inspect Preview舍沙,可以打開窗口屬性窗口,可以設(shè)置預(yù)覽窗口屬性
    第四個按鈕:Duplicate Preview 剔宪,可以復(fù)制創(chuàng)建多個預(yù)覽窗口

  2. 代碼控制預(yù)覽的機型

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

// 多設(shè)備同時預(yù)覽
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. 組合靜態(tài)的View和動態(tài)的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)
        }
    }
}

如果遍歷的對象沒有實現(xiàn)Identifiable協(xié)議,則需要傳id

List {
    ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
        Text(key)
    }
}
  1. ObservableObject協(xié)議
    當(dāng)遵守ObservableObject協(xié)議的數(shù)據(jù)更新時葱绒,綁定數(shù)據(jù)的view會自動更新
final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
}
  1. @Published
    使用@Published修飾監(jiān)聽對象的屬性感帅,表示該對象的屬性需要把屬性值的改變更新進(jìn)去
final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
}
  1. @StateObject
    使用@StateObject初始化一個監(jiān)聽對象的數(shù)據(jù),
    使用.environmentObject把數(shù)據(jù)設(shè)置到環(huán)境對象里地淀,
    在需要的地方去取環(huán)境對象@EnvironmentObject var modelData: ModelData進(jìn)行使用
@main
struct MySwiftUIApp: App {
    // 定義
    @StateObject private var modelData = ModelData()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
            // 設(shè)置
                .environmentObject(modelData)
        }
    }
}

// 取
@EnvironmentObject var modelData: ModelData

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

  2. @EnvironmentObject屬性用于在下級view中接收傳遞過來的參數(shù)

  3. environmentObject(_:)方法用于向下級傳遞參數(shù)

  4. @Binding:綁定修飾符用于修飾一個值失球,這個值用來改變另外一個值

綁定:
@Binding var isSet: Bool

修改:
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
  1. 定義有狀態(tài)的字段,使用@State修飾帮毁,定義為private实苞,并且賦初始值
    @State private var showFavoritesOnly = false

  2. @State
    使用@State屬性為應(yīng)用程序中的數(shù)據(jù)建立一個真實來源,您可以從多個視圖中修改這些數(shù)據(jù)烈疚。SwiftUI管理底層存儲并根據(jù)值自動更新視圖黔牵。

  3. 使用Path直接繪制,可以當(dāng)做View來使用

  4. View動畫爷肝,包括顏色猾浦、透明度、旋轉(zhuǎn)灯抛、大小和其他屬性等金赦,可以使用withAnimation來包裹狀態(tài)State實現(xiàn)動畫

withAnimation {
    self.showDetail.toggle()
}

withAnimation(.easeInOut(duration: 4)) {
    self.showDetail.toggle()
}
  1. 調(diào)用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用來調(diào)整一個viewlist中的上下左右間距

  2. CaseIterable,用在enum中用來獲取allCases方法

  3. @Environment对嚼,SwiftUI提供了在環(huán)境中值的存儲夹抗,使用@Environment可以訪問值,并可以讀寫

@Environment(\.editMode) var editMode
  1. @State 纵竖、@ObservableObject漠烧、@Binding@EnvironmentObject區(qū)別
    @State@ObservableObject之間有一些細(xì)微的差異磨确。這些都是很重要的沽甥,因為它們都有不同的用途声邦。
    @State在視圖本地乏奥。值或數(shù)據(jù)在視圖中本地保存。它由框架管理亥曹,由于它存儲在本地邓了,因此它是一個值類型恨诱。
    使用@State來存儲不斷變化的數(shù)據(jù)。記住骗炉,我們所有的SwiftUI視圖都是結(jié)構(gòu)體照宝,這意味著沒有@State之類的東西就無法更改它們。
    @ObservableObject在視圖外部句葵,并且不存儲在視圖中厕鹃。它是一種引用類型,因為它不在本地存儲乍丈,而只是具有對該值的引用剂碴。這不是由框架自動管理的,而是開發(fā)人員的責(zé)任轻专。這最適用于外部數(shù)據(jù)忆矛,例如數(shù)據(jù)庫或由代碼管理的模型。
    @Binding也在視圖內(nèi)请垛,但是與@State區(qū)別在于@Binding用于不通視圖之間的參數(shù)傳遞催训。@Binding@ObservedObject一樣都是傳遞引用。
    @EnvironmentObject 可以理解為全局變量

  2. ObservableObject@Published
    遵循 ObservableObject 協(xié)議的類可以使用 SwiftUI@Published 屬性包裝器來自動發(fā)布屬性的變化宗收,以便使用該類的實例的任何視圖能夠自動重新調(diào)用 body 屬性漫拭,保持界面與數(shù)據(jù)的一致。
    @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可設(shè)置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. 添加導(dǎo)航欄使用navigationBarTitle挑宠,displayMode設(shè)置顯示樣式
NavigationView {
            Form {
                Section {
                    Text("Hello World")
                }
            }
            .navigationBarTitle("SwiftUI", displayMode: .inline)
        }
  1. 就像SwiftUI的其他視圖一樣各淀,VStack最多可以有10個子節(jié)點——如果您想添加更多子節(jié)點,應(yīng)該將它們包裝在一個Group中诡挂。

  2. Color.red本身就是一個視圖碎浇,這就是為什么它可以像形狀和文本一樣使用。它會自動占用所有可用空間璃俗。

  3. Color.primarySwiftUI中文本的默認(rèn)顏色奴璃,根據(jù)用戶的設(shè)備是在亮模式還是在暗模式下運行,它將是黑色還是白色城豁。還有Color.secondary苟穆,它也可以是黑色或白色,這取決于設(shè)備,但現(xiàn)在它有輕微的透明度雳旅,以便后面的一點顏色可以穿透跟磨。

  4. 如果要將內(nèi)容置于安全區(qū)域之下攒盈,可以使用edgesIgnoringSafeArea()修飾符指定要運行到的屏幕邊緣。

Color.red.ignoresSafeArea()
Color.red.edgesIgnoringSafeArea(.all)
  1. 漸變色
VStack {
            // 線性漸變 LinearGradient 沿一個方向運行,因此我們?yōu)槠涮峁┝艘粋€起點和終點
            LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .leading, endPoint: .trailing)
            // 徑向漸變 RadialGradient 以圓形向外移動,因此腐宋,我們沒有指定方向扯键,而是指定了起點和終點半徑——顏色應(yīng)從圓心到圓心的距離開始和停止變化
            RadialGradient(gradient: Gradient(colors: [.blue, .black]), center: .center, startRadius: 20, endRadius: 200)
            // 角度漸變 AngularGradient蔬捷,盡管您可能聽說過其他地方將其稱為圓錐形或圓錐形漸變凰兑。這使顏色圍繞一個圓圈循環(huán)而不是向外輻射
            AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center)
        }
image.png
  1. 如果您發(fā)現(xiàn)圖像已被某種顏色填充,例如顯示為純藍(lán)色而不是實際圖片旅择,則可能是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("標(biāo)題"), message: Text("文本內(nèi)容"), primaryButton: .cancel {
                    print("點擊取消")
                }, secondaryButton: .default(Text("確定")) {
                    print("點擊確定")
                })
            })
        }
    }
}
image.png
  1. Swift內(nèi)置形狀:
    矩形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為什么使用結(jié)構(gòu)體而不是用類?

首先冯键,有一個性能因素:結(jié)構(gòu)體比類更簡單,更快。我之所以說性能因素,是因為很多人認(rèn)為這是SwiftUI使用結(jié)構(gòu)體的主要原因啃匿,而實際上這只是更大范圍的一部分。

UIKit中,每個視圖都來自一個名為UIView的類,該類具有許多屬性和方法:背景色扒秸,確定其放置方式的約束写烤,用于將其內(nèi)容呈現(xiàn)到其中的圖層等等。其中有很多拾徙,每個UIViewUIView子類都必須具有它們洲炊,因為繼承是這樣工作的。

視圖作為結(jié)構(gòu)體還是有很多更重要的事情:它迫使我們考慮以一種干凈的方式隔離狀態(tài)。您會發(fā)現(xiàn),類能夠自由更改其值膝但,這可能導(dǎo)致代碼混亂箩帚。

通過生成不會隨時間變化的視圖,SwiftUI鼓勵我們轉(zhuǎn)向更具功能性的設(shè)計方法:在將數(shù)據(jù)轉(zhuǎn)換為UI時,我們的視圖變成簡單的舍杜,惰性的東西频丘,而不是會失去控制的智能化的東西窖张。

當(dāng)您查看可以作為視圖的事物時喜庞,可以看到這一點嫉你。我們已經(jīng)使用了Color.redLinearGradient作為視圖——包含很少數(shù)據(jù)的簡單類型饲梭。實際上析苫,您不能找到比使用Color.red作為視圖的更好的主意:除了“用紅色填充我的空間”之外兜叨,它不包含任何信息。

相比之下衩侥,Apple的UIView文檔列出了UIView擁有的約200種屬性和方法国旷,無論是否需要它們,所有這些屬性和方法都將傳遞給其子類茫死。

提示:如果您在視圖中使用類跪但,則可能會發(fā)現(xiàn)代碼無法編譯或在運行時崩潰。

  1. SwiftUI應(yīng)用點語法修改視圖峦萎,返回的也是視圖類型屡久。每次我們修改視圖時忆首,SwiftUI都會使用以下泛型來應(yīng)用該修飾符:ModifiedContent<OurThing, OurModifier>

  2. 為什么SwiftUI使用 some View 作為視圖類型被环?
    返回some View與僅返回View相比有兩個重要區(qū)別:

  • 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包裝器創(chuàng)建新視圖的方式漠酿, Text(…)Text(…).background(Color.red)是不同的底層類型冯凹,這與some View不兼容。

SwiftUI使用ModifiedContent構(gòu)建數(shù)據(jù)的方式炒嘲。
SwiftUI是如何處理VStack這樣的東西的——它符合View協(xié)議宇姚,如果您創(chuàng)建了一個包含兩個文本視圖的VStack,那么SwiftUI會無聲地創(chuàng)建一個TupleView來包含這兩個視圖夫凸。TupleView的一個版本可以跟蹤十種不同的內(nèi)容:

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))
    }
}

// 自定義修飾符,并重新構(gòu)建視圖
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使用區(qū)別:
let agents = ["Cyril", "Lana", "Pam", "Sterling"]
VStack {
    ForEach(0 ..< agents.count) {
        Text(self.agents[$0])
    }
}

我們回到Swift如何識別數(shù)組中的值蒜绽。當(dāng)我們使用0..<50..<agents.count這樣的范圍時,Swift確信每個項目都是唯一的桶现,因為它將使用范圍中的數(shù)字——每個數(shù)字在循環(huán)中只使用一次躲雅,所以它肯定是唯一的。
但是當(dāng)使用字符串時骡和,不會標(biāo)識為唯一相赁,導(dǎo)致body被調(diào)用時會被重建。因此可以使用id來標(biāo)識慰于,如下:

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

另外钮科,為了標(biāo)識視圖的唯一,可以用Identifiable協(xié)議來實現(xiàn):

定義:

@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)")
        }
    }
}

因此婆赠,該綁定實際上只是作為一個傳遞——它本身不存儲或計算任何數(shù)據(jù)绵脯,而是作為UI和被操縱的底層狀態(tài)值之間的一個填充。

但是,請注意桨嫁,選擇器現(xiàn)在是使用selection:binding進(jìn)行的植兰,不需要美元符號。我們不需要在這里明確要求雙向綁定璃吧,因為它已經(jīng)是了楣导。

高級使用:

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. double轉(zhuǎn)String保留幾位小數(shù)
// 保留2位小數(shù)
Text("\(sleepAmount, specifier: "%.2f") 小時")
 // 保留2位小數(shù),并自動刪除末尾不需要的0
Text("\(sleepAmount, specifier: "%.2g") 小時")
image.png
  1. DatePicker使用
struct DatePickerView: View {
    @State private var wakeUp = Date()
    var body: some View {
        VStack {
            // 有標(biāo)題
            DatePicker("Please enter a date", selection: $wakeUp)
            // 無標(biāo)題
            DatePicker("Please enter a date", selection: $wakeUp)
                .labelsHidden()
            // 無標(biāo)題畜挨,有時間范圍
            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轉(zhuǎn)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. 顯示視圖時調(diào)用的閉包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對不同的動畫巴元,進(jìn)行分別處理
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 {
                        // 實時根據(jù)手勢位置改變視圖的位置
                        self.dragAmount = $0.translation
                    }
                    .onEnded { _ in
                        // 彈性動畫歸0
                        withAnimation(.spring()) {
                            self.dragAmount = .zero
                        }
                    }
            )
    }
}
  1. 結(jié)構(gòu)體和類的使用區(qū)別
  • 結(jié)構(gòu)體一直擁有唯一的所有者毡咏,而對于類,多個對象可以指向同一個值逮刨。
  • 類不需要在更改其屬性的方法之前使用mutating關(guān)鍵字呕缭,因為您可以更改常量類的屬性。

實際上修己,這意味著恢总,如果我們有兩個SwiftUI視圖,并且我們將同一個結(jié)構(gòu)體發(fā)送給它們睬愤,那么它們實際上都有該結(jié)構(gòu)體的唯一副本片仿;如果其中一個更改了它,那么另一個將看不到該更改尤辱。另一方面砂豌,如果我們創(chuàng)建一個類的實例并將其發(fā)送到兩個視圖,它們將共享更改光督。

對于SwiftUI開發(fā)人員來說阳距,這意味著如果我們想要在多個視圖之間共享數(shù)據(jù)——如果我們想要兩個或多個視圖指向同一個數(shù)據(jù),以便在一個更改時它們都得到這些更改——我們需要使用類而不是結(jié)構(gòu)體可帽。

  1. 為什么使用@ObservedObject
    如果使用類的同時娄涩,使用@State來讓SwiftUI監(jiān)聽值的改變,雖然值改變了映跟,但是SwiftUI監(jiān)聽不到類中值的改變,不會對body進(jìn)行銷毀和重建扬虚,所以需要使用@OvervedObject來處理該問題努隙。

當(dāng)我們使用@State時,我們要求SwiftUI監(jiān)視屬性的更改辜昵。因此荸镊,如果我們更改字符串、翻轉(zhuǎn)布爾值、添加到數(shù)組等躬存,則屬性已更改张惹,SwiftUI將重新調(diào)用視圖的body屬性。

當(dāng)User是一個結(jié)構(gòu)體時岭洲,每次我們修改該結(jié)構(gòu)體的屬性時宛逗,Swift實際上是在創(chuàng)建該結(jié)構(gòu)的新實例。@State能夠發(fā)現(xiàn)這個變化盾剩,并自動重新加載我們的視圖±准ぃ現(xiàn)在我們有了一個類,這種行為不再發(fā)生:Swift可以直接修改值告私。

還記得我們?nèi)绾螢樾薷膶傩缘慕Y(jié)構(gòu)體方法使用mutating關(guān)鍵字嗎屎暇?這是因為,如果我們將結(jié)構(gòu)體的屬性創(chuàng)建為變量驻粟,但結(jié)構(gòu)體本身是常量根悼,則無法更改屬性——Swift需要能夠在屬性更改時銷毀和重新創(chuàng)建整個結(jié)構(gòu)體,而常量結(jié)構(gòu)則不可能這樣做蜀撑。類不需要mutating關(guān)鍵字番挺,因為即使類實例標(biāo)記為常量Swift,仍然可以修改變量屬性屯掖。

我知道這聽起來非常理論化玄柏,但這里有個問題:既然User是一個類,那么屬性本身不會改變贴铜,所以@State不會注意到任何事情粪摘,也無法重新加載視圖。是的绍坝,類中的值正在更改徘意,但是@State不監(jiān)視這些值,所以實際上發(fā)生的情況是轩褐,類中的值正在更改椎咧,但視圖不會重新加載以反映該更改。

為了解決這個問題把介,是時候拋棄@State了勤讽。相反,我們需要一個更強大的屬性包裝器拗踢,名為@ObservedObject脚牍。

  1. @ObservedObject@Published的使用
    如果需要在多個視圖之間共享數(shù)據(jù)的話巢墅,可使用@ObservedObject@EnvironmentObject诸狭。

@Published用于類中券膀,通知關(guān)注類的所有視圖在類發(fā)生改變時,去重新加載驯遇。

class User {
    // @Published通知關(guān)注類的所有視圖在類發(fā)生改變時芹彬,去重新加載
    @Published var firstName = "zhiying"
    @Published var lastName = "yuan"
}

@ObservedObject用于獲知知道哪些類在改變時能通知視圖,它告訴SwiftUI監(jiān)視類中的任何的更改公告叉庐。

@ObservedObject屬性包裝器只能用于符合ObservableObject協(xié)議的類型舒帮。該協(xié)議沒有任何要求,實際上意味著“我們希望其他事物能夠監(jiān)視此更改”眨唬。

// 類遵守ObservableObject協(xié)議
class User: ObservableObject {
    // @Published 通知關(guān)注類的所有視圖在類發(fā)生改變時会前,去重新加載
    @Published var firstName = "zhiying"
    @Published var lastName = "yuan"
}

struct ContentView: View {
    // @ObservedObject 用于標(biāo)記哪些類在改變時通知視圖加載視圖
    @ObservedObject var user = User()
    
    var body: some View {
        VStack {
            Text("名字是\(user.firstName)\(user.lastName)")
            
            TextField("firstName", text: $user.firstName)
            TextField("lastName", text: $user.lastName)
        }
    }
}

三個步驟:

  • 創(chuàng)建一個符合ObservableObject協(xié)議的類。
  • @Published標(biāo)記一些屬性匾竿,以便使用該類的所有視圖在更改時都得到更新瓦宜。
  • 使用@ObservedObject屬性包裝器創(chuàng)建我們的類的實例。
  1. 彈出模態(tài)視圖岭妖,并通過獲取全局變量來關(guān)閉模態(tài)視圖

彈出

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

關(guān)閉

struct SecondView: View {
    // 獲取全局環(huán)境變量 presentationMode
    @Environment(\.presentationMode) var secondPresentationMode

    var body: some View {
        Button("關(guān)閉") {
            // 通過獲取到的全局環(huán)境變量临庇,來關(guān)閉模態(tài)視圖
            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來創(chuàng)建動態(tài)視圖的不同方式昵慌,它們都有一個共同點:SwiftUI 需要知道如何唯一識別動態(tài)視圖的每一項假夺,以便正確地動畫化改變。

如果一個對象遵循Identifiable協(xié)議斋攀,那么 SwiftUI 會自動使用它的id屬性作為唯一標(biāo)識已卷。如果我們不使用Identifiable,那就需要指定一個我們知道是唯一的屬性的 key path淳蔼,比如圖書的 ISBN 號侧蘸。但假如我們不遵循Identifiable也沒有唯一的 key path,我們通常會使用.self鹉梨。

我們對原始數(shù)據(jù)類型讳癌,例如Int和String使用過.self,就像下面這樣:

List {
ForEach([2, 4, 6, 8, 10], id: .self) {
Text("($0) is even")
}
}
對于 Core Data 為我們生成托管類存皂,我們也使用.self晌坤,當(dāng)時我沒有解釋這是如何工作的床牧,或者說它究竟是如何與我們的ForEach關(guān)聯(lián)的宁昭。不過今天我要再來討論這個問題,因為我覺得這會給你提供一些有助益的洞察己沛。

當(dāng)我們使用.self作為標(biāo)識符時猜憎,我們指的是“整個對象”娩怎,但實踐上這個指代并沒有包含太多的含義 —— 一個結(jié)構(gòu)體就是結(jié)構(gòu)體,除了內(nèi)容之外胰柑,它并沒有包含任何特定的標(biāo)識信息截亦。因此,實際發(fā)生的事情是柬讨,Swift 計算了結(jié)構(gòu)體的哈希值 —— 一種以固定長度的值表示復(fù)雜數(shù)據(jù)的方法 —— 然后以哈希值作為標(biāo)識符崩瓤。

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

無論輸入的數(shù)據(jù)多大踩官,輸出總是固定大小却桶。
對同一個對象計算兩次哈希值,應(yīng)該返回相同的值蔗牡。
這兩點聽起來很簡單颖系,但你思考一下:假設(shè)我們獲取 “Hello World” 的哈希值和莎士比亞全集的哈希值,兩者都將是相同的大小辩越。這意味著我們是無法從哈希值還原出原始數(shù)據(jù)的 —— 我們無法從 40 個看起來完全隨機的十六進(jìn)制數(shù)字轉(zhuǎn)換出莎士比亞全集嘁扼。

哈希常見于數(shù)據(jù)校驗。舉個例子黔攒,假如你下載了一個 8GB 的 zip 文件趁啸,你可以通過對比你本地的哈希值和服務(wù)器上的哈希值來確認(rèn)文件是正確的 —— 如果兩者匹配,說明 zip 文件是一致的督惰。哈希還被用于字典的鍵和值不傅,這是為什么字典查詢速度很快的原因。

上面說的這些很重要赏胚,因為 Xcode 為我們生成托管對象的類時访娶,它會讓這些類遵循Hashable,這是一個表示 Swift 可以從中生成哈希值的協(xié)議觉阅,也就是說崖疤,我們可以用它的.self作為標(biāo)識符。這也是為什么String和Int可以用.self的原因:它們也遵循Hashable留拾。

Hashable有點類似Codable:如果我們想讓一個自定義類型遵循Hashable戳晌,那么只要它包含的所有東西也遵循Hashable,那我們就不必做額外的工作痴柔。為了說明這一點沦偎,我們可以創(chuàng)建一個自定義結(jié)構(gòu)體,讓它遵循Hashable而不是Identifiable咳蔚,然后使用.self來標(biāo)識它:

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豪嚎,因為它所有的屬性都已經(jīng)遵循Hashable,因此 Swift 會計算每個屬性的哈希值谈火,然后結(jié)合這些值產(chǎn)生一個代表整個對象的哈希值侈询。當(dāng)然,如果我們遇到兩個同名的學(xué)生糯耍,那我們可能會遇到問題扔字,這就像我們擁有一個包含兩個相同字符串的字符串?dāng)?shù)組一樣囊嘉。

現(xiàn)在,你可能想革为,這樣會導(dǎo)致問題吧:如果我們用相同的值創(chuàng)建了兩個 Core Data 對象扭粱,它們會生成相同的哈希值,這樣我們就遇到問題了震檩。不過琢蛤,其實 Core Data 是一種很聰明的方式來工作:它為我們創(chuàng)建的對象實際上有一組我們定義數(shù)據(jù)模型時定義的屬性之外的其他屬性,包括一個叫 ID 的對象 —— 這是一個可以唯一標(biāo)識對象的標(biāo)識符抛虏,不管對象包含的屬性是什么博其。這些 ID 類似于 UUID,在我們創(chuàng)建對象時迂猴,Core Data 順序產(chǎn)生它們慕淡。

因此,.self適用于所有遵循Hashable的類错忱,因為 Swift 會為其生成哈希值并用該值作為對象的唯一標(biāo)識儡率。這對于 Core Data 的對象同樣適用,因為它們已經(jīng)遵循了Hashable以清。

警告: 雖然給一個對象計算兩次哈希值應(yīng)該返回相同的值儿普,但在兩次應(yīng)用運行期間計算它們 —— 比如說,計算哈希值掷倔,退出應(yīng)用眉孩,重啟,然后再計算哈希值 —— 是有可能返回不同的值的勒葱。

  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數(shù)組創(chuàng)建的浪汪,可以直接將索引集直接傳給numbers數(shù)組
                    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. 本地數(shù)據(jù)存儲UserDefaults
存
UserDefaults.standard.setValue(self.tapCount, forKey: "tapCount")
取(未設(shè)置有默認(rèn)值)
UserDefaults.standard.integer(forKey: "tapCount")
  1. 調(diào)整圖片大小凛虽,以適應(yīng)屏幕
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("我是標(biāo)題"))
        }
    }
}
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("我是標(biāo)題"))
        }
    }
}
image.png
  1. List + ForEachScrollView + ForEach區(qū)別
    List + ForEach會在可見的時候才創(chuàng)建
    ScrollView + ForEach會一次性創(chuàng)建所有視圖

List + ForEach

struct ContentView: View {
    var body: some View {
        NavigationView {
            // List + ForEach 會在可見的時候才創(chuàng)建
            List {
                ForEach(0..<100) {
                    CustomText("\($0) _")
                }
            }
            .navigationTitle(Text("標(biāo)題"))
        }
    }
}

ScrollView + ForEach

struct ContentView: View {
    var body: some View {
        NavigationView {
            ScrollView {
                ForEach(0..<100) {
                    CustomText("\($0) _")
                }
                .frame(maxWidth: .infinity)
            }
            .navigationTitle(Text("標(biāo)題"))
        }
    }
}
  1. 布局優(yōu)先級layoutPriority
    所有視圖的默認(rèn)優(yōu)先級均為0
struct ContentView: View {
    var body: some View {
        HStack(spacing: 16) {
            Text("Hello")
            Text("World")
            // 布局優(yōu)先級layoutPriority死遭,所有視圖的默認(rèn)優(yōu)先級均為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使用
    從起始值以指定值步幅到結(jié)束值的序列
從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. 循環(huán)繪制形狀
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向上計數(shù)到 pi * 2凯旋,每次移動 pi / 8
        for number in stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 8) {
            // 根據(jù)循環(huán)旋轉(zhuǎn)當(dāng)前的花瓣
            let rotation = CGAffineTransform(rotationAngle: number)

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

            // 使用我們的屬性以及固定的Y和高度為該花瓣創(chuàng)建路徑
            let originalPetal = Path(ellipseIn: CGRect(x: CGFloat(petalOffset), y: 0, width: CGFloat(petalWidth), height: rect.width / 2))

            // 將我們的旋轉(zhuǎn)/位置變換應(yīng)用于花瓣
            let rotatedPetal = originalPetal.applying(position)

            // 將其添加到我們的主路徑
            path.addPath(rotatedPetal)
        }

        // 現(xiàn)在將主徑 return
        return path
    }
}
image.png
  1. ImagePaint 制作邊框和填充
struct ContentView: View {
    var body: some View {
        VStack {
// sourceRect 相對大小和位置的CGRect 0表示“開始”呀潭,1表示“結(jié)束”
// 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默認(rèn)使用CoreAnimation來進(jìn)行渲染至非,但是遇到復(fù)雜的渲染钠署,可以啟用高性能渲染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

通過應(yīng)用一個稱為drawingGroup()的新修改器來解決此問題荒椭。這告訴SwiftUI谐鼎,在將視圖內(nèi)容作為單個呈現(xiàn)的輸出放回到屏幕上之前,應(yīng)將視圖的內(nèi)容呈現(xiàn)到屏幕外的圖像中(離屏渲染)趣惠,這要快得多狸棍。在幕后身害,該功能由Metal提供支持,MetalApple的框架隔缀,可直接與GPU協(xié)同工作以實現(xiàn)極快的圖形题造。

重要提示:drawingGroup()修飾符有助于了解和保留您的武器庫傍菇,這是解決性能問題的一種方法猾瘸,但是您不應(yīng)該經(jīng)常使用它。添加屏幕外渲染過程可能會降低SwiftUI進(jìn)行簡單繪圖的速度丢习,因此牵触,在嘗試引入drawingGroup()之前,應(yīng)等到遇到實際性能問題后再進(jìn)行操作咐低。

74揽思、實現(xiàn)實施模糊、混合模式见擦、飽和度調(diào)整等效果
SwiftUI使我們能夠出色地控制視圖的呈現(xiàn)方式钉汗,包括應(yīng)用實時模糊,混合模式鲤屡,飽和度調(diào)整等功能损痰。

混合模式使我們可以控制一個視圖在另一個視圖上的渲染方式。默認(rèn)模式是.normal酒来,它只是將新視圖中的像素繪制到后面的任何東西上卢未,但是有很多選項可以控制顏色和不透明度。

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

之所以命名為“Multiply”堰汉,是因為它將每個源像素顏色與目標(biāo)像素顏色相乘——在我們的示例中辽社,是圖像的每個像素和頂部的矩形的每個像素。每個像素具有RGBA的顏色值翘鸭,范圍從0(沒有該顏色)到1(所有顏色)滴铅,因此所得的最高顏色為1x1,最低的顏色為0x0就乓。

對純色使用乘法會產(chǎn)生一種非常常見的色調(diào)效果:黑色保持黑色(因為它們的顏色值為0汉匙,所以無論您將頂部乘以0都將產(chǎn)生0),而較淺的顏色會變成各種陰影著色档址。

混合模式screen盹兢,它的作用與乘法相反:將顏色反轉(zhuǎn),執(zhí)行乘法守伸,然后再次反轉(zhuǎn)顏色绎秒,從而產(chǎn)生較亮的圖像而不是較暗的圖像。

常用用法:.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)) // 飽和度尼摹,用于調(diào)整顏色的數(shù)量
                .blur(radius: (1 - amount) * 20)
            
            Slider(value: $amount)
                            .padding()
        }
    }
}
image.png

75见芹、edgesIgnoringSafeArea邊界忽略safeArea安全區(qū)域

76剂娄、Shape形狀設(shè)置動畫(單個動畫變量)

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 給形狀設(shè)置動畫
    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
   }
}

這里發(fā)生的事情非常復(fù)雜:當(dāng)我們使用withAnimation()時,SwiftUI會立即將狀態(tài)屬性更改為其新值玄呛,但在幕后阅懦,隨著動畫的進(jìn)行,它還在跟蹤隨時間的值變化徘铝。隨著動畫的進(jìn)行耳胎,SwiftUI會將 Shape 的animatableData屬性設(shè)置為最新值,這取決于我們來決定這意味著什么——在本例中惕它,我們將其直接分配給insetAmount怕午,因為這就是我們要進(jìn)行動畫處理的東西。

記住淹魄,SwiftUI在應(yīng)用動畫之前先評估視圖狀態(tài)郁惜,然后再應(yīng)用動畫〖孜可以看到我們最初有評估為Trapezoid(insetAmount:50)的代碼兆蕉,但是在選擇了一個隨機數(shù)之后,我們最終得到了(例如)Trapezoid(insetAmount:62)缤沦。因此虎韵,它將在動畫的整個長度內(nèi)插值50到62,每次將形狀的animatableData屬性設(shè)置為最新的插值:51疚俱、52呆奕、53,依此類推梁钾,直到達(dá)到62姆泻。

77零酪、Shape形狀設(shè)置動畫(多個動畫變量)

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)

        // 循環(huán)遍歷所有行和列拇勃,從而使交替的正方形變?yōu)椴噬?        for row in 0..<rows {
            for column in 0..<columns {
                if (row + column).isMultiple(of: 2) {
                    // 這個正方形應(yīng)該是彩色的方咆;在此處添加一個矩形
                    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包含了所有的數(shù)據(jù)
        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>,是一個包含字符串的可發(fā)布的對象。

79揪惦、使用URLSessionURLRequest請求數(shù)據(jù)

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創(chuàng)建自定義視圖措左,實現(xiàn)雙向綁定

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 ? "開啟": "關(guān)閉")
            }
            .navigationBarTitle("Cupcake Corner")
        }
    }
}

如果不使用@Binding,外部頁面中凉逛,使用外部頁面的屬性創(chuàng)建自定義頁面,只是傳入自定義頁面參數(shù)昔瞧,傳入后自晰,里面值的改變并不會傳遞到外面搓劫。

自定義頁面中枪向,將需要綁定的屬性使用@Binding修飾符,綁定外部頁面的屬性深员,將自定義頁面中的值的改變傳遞到外部頁面中倦畅,同步改變。

82芭概、使用CoreData來增刪改查數(shù)據(jù)
相關(guān)文章:SwiftUI CoreData入門概念和基礎(chǔ)大全

創(chuàng)建持久化控制器單例PersistenceController,并創(chuàng)建初始化持久化容器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中創(chuàng)建全局持久化控制器,并將持久化控制器的持久化容器的上下文注入全局環(huán)境變量中
import SwiftUI

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

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}
創(chuàng)建項目同名的DataModel文件,后綴為.xcdatamodelId触机,創(chuàng)建entity
image.png
從全局環(huán)境變量中取出上下文

@Environment(\.managedObjectContext) private var viewContext

使用@FetchRequest裝飾器片任,從數(shù)據(jù)庫中讀取指定entity的數(shù)據(jù)列表
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
往coredata添加數(shù)據(jù),通過從全局環(huán)境變量中獲取到的上下文产场,創(chuàng)建對象
    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數(shù)據(jù)
    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)")
            }
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骗奖,一起剝皮案震驚了整個濱河市重归,隨后出現(xiàn)的幾起案子鼻吮,更是在濱河造成了極大的恐慌,老刑警劉巖香椎,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異玛界,居然都是意外死亡,警方通過查閱死者的電腦和手機笨枯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門严嗜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漫玄,“玉大人,你說我怎么就攤上這事刨秆。” “怎么了缓醋?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抗俄。 經(jīng)常有香客問我,道長胰蝠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任翔横,我火速辦了婚禮效览,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘦锹。我一直安慰自己弯院,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鼠证,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娩鹉。 梳的紋絲不亂的頭發(fā)上戚宦,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天垦搬,我揣著相機與錄音猴贰,去河邊找鬼瑟捣。 笑死,一個胖子當(dāng)著我的面吹牛桑李,可吹牛的內(nèi)容都是我干的贵白。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼磷蜀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起庶弃,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤梆造,失蹤者是張志新(化名)和其女友劉穎镇辉,沒想到半個月后村砂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汛骂,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡苫幢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年诈悍,在試婚紗的時候發(fā)現(xiàn)自己被綠了适袜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡疫萤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尾序,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布尚揣,位于F島的核電站快骗,受9級特大地震影響名秀,放射性物質(zhì)發(fā)生泄漏继榆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秽之。 院中可真熱鬧,春花似錦董虱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽对人。三九已至,卻和暖如春蛇捌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背春贸。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工瓮恭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屯蹦,地道東北人阔挠。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓谴仙,卻偏偏與公主長得像揩局,于是被迫代替她去往敵國和親凌盯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容