學(xué)習(xí)文章
- 文集:Hacking with iOS: SwiftUI Edition
- SwiftUI對比Flutter
- 八個常見的SwiftUI誤用及對應(yīng)的正確打開方式
- SwiftUI 基礎(chǔ)之 Identifiable
- SwiftUI - Data Binding注解
SwiftUI
要求
iOS13.0+
快捷鍵
-
control + option + 點擊
:出現(xiàn)屬性編輯器 -
command + 點擊
:出現(xiàn)快捷菜單 -
command + shift + L
:Show Library
彈窗
布局
-
VStack
- 垂直布局 -
HStack
- 水平布局 -
Spacer
- 間距 -
Text
- 文本 -
Image
- 圖片 -
Divider
- 分割線 -
Group
- 組 -
ScrollView
- 滾動視圖 -
Path
- 路徑 -
Shaper
- 形狀 -
Form
劈彪、Section
- 表單 -
Color.red
- 填充顏色 -
ForEach
- 循環(huán) -
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()
}
}
-
ContentView
為布局失尖,ContentView_Previews
為預(yù)覽布局 -
body
為計算屬性忌穿,類型為不透明類型的View
束莫,不透明類型使用some
修飾 -
Swift
語法碧注,只有一行代碼竖哩,return
可以省略 -
some
修飾嘶窄,表示不透明類型怀跛,總會返回某一個特定的類型,但是不知道是哪一種
- 可以返回關(guān)聯(lián)類型的協(xié)議類型
- 安全性:不對外暴露具體的返回類型
- 用來解決
SwiftUI
繁瑣的不確定返回類型問題
使用技巧
可以在右上角+里拖動空間到代碼中
使用
import
導(dǎo)入所需的庫可以新建
SwiftUI View
ignoresSafeArea
忽略safeArea
的邊距柄冲,用在feame
前布局group組件可增加
padding
VStack
可添加font
吻谋、foregroundColor
等屬性,對所有包含的元素起效串聯(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.
- 使用
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))
}
}
-
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
- 頁面跳轉(zhuǎn)使用
NavigationLink
低千,destination
為要跳轉(zhuǎn)的頁面
NavigationLink(destination: LandmarkDetail()) {
LandmarkRow(landmark: landmark)
}
- 使用
NavigationView
為頁面田健導(dǎo)航欄,可設(shè)置navigationTitle
等
NavigationView {
List(landmarks) { landmark in
NavigationLink(destination: LandmarkDetail()) {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
預(yù)覽窗口按鈕作用:
第一個按鈕:Live Preview
和Debug Preview
馏颂,未打開只能查看頁面示血,不能點擊等棋傍,打開之后可以點擊跳轉(zhuǎn)頁面等交互操作
第二個按鈕:Preview On Device
,連上真機點擊之后难审,預(yù)覽可以同步在真機上展示
第三個按鈕:Inspect Preview
舍沙,可以打開窗口屬性窗口,可以設(shè)置預(yù)覽窗口屬性
第四個按鈕:Duplicate Preview
剔宪,可以復(fù)制創(chuàng)建多個預(yù)覽窗口代碼控制預(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)
}
}
}
- 組合靜態(tài)的
View
和動態(tài)的view
到List
里拂铡,可使用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)
}
}
-
ObservableObject
協(xié)議
當(dāng)遵守ObservableObject
協(xié)議的數(shù)據(jù)更新時葱绒,綁定數(shù)據(jù)的view
會自動更新
final class ModelData: ObservableObject {
@Published var landmarks: [Landmark] = load("landmarkData.json")
}
-
@Published
使用@Published
修飾監(jiān)聽對象的屬性感帅,表示該對象的屬性需要把屬性值的改變更新進(jìn)去
final class ModelData: ObservableObject {
@Published var landmarks: [Landmark] = load("landmarkData.json")
}
-
@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
Bool
的toggle()
方法:在true
和false
之前切換@EnvironmentObject
屬性用于在下級view中接收傳遞過來的參數(shù)environmentObject(_:)
方法用于向下級傳遞參數(shù)@Binding
:綁定修飾符用于修飾一個值失球,這個值用來改變另外一個值
綁定:
@Binding var isSet: Bool
修改:
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
定義有狀態(tài)的字段,使用
@State
修飾帮毁,定義為private
实苞,并且賦初始值
@State private var showFavoritesOnly = false
@State
:
使用@State
屬性為應(yīng)用程序中的數(shù)據(jù)建立一個真實來源,您可以從多個視圖中修改這些數(shù)據(jù)烈疚。SwiftUI
管理底層存儲并根據(jù)值自動更新視圖黔牵。使用
Path
直接繪制,可以當(dāng)做View來使用View
動畫爷肝,包括顏色猾浦、透明度、旋轉(zhuǎn)灯抛、大小和其他屬性等金赦,可以使用withAnimation
來包裹狀態(tài)State
實現(xiàn)動畫
withAnimation {
self.showDetail.toggle()
}
withAnimation(.easeInOut(duration: 4)) {
self.showDetail.toggle()
}
- 調(diào)用
View
的transition
可以為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)
}
}
ListRowInsets
用來調(diào)整一個view
在list
中的上下左右間距CaseIterable
,用在enum
中用來獲取allCases
方法@Environment
对嚼,SwiftUI
提供了在環(huán)境中值的存儲夹抗,使用@Environment
可以訪問值,并可以讀寫
@Environment(\.editMode) var editMode
@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
可以理解為全局變量ObservableObject
和@Published
遵循ObservableObject
協(xié)議的類可以使用SwiftUI
的@Published
屬性包裝器來自動發(fā)布屬性的變化宗收,以便使用該類的實例的任何視圖能夠自動重新調(diào)用body
屬性漫拭,保持界面與數(shù)據(jù)的一致。
@Published var profile = Profile.default
界面中使用@Binding
來綁定UI
``使用
UIViewRepresentable
來將UIKit
中已有的View
在SwiftUI
中使用
使用UIViewControllerRepresentable
來UIKit
中的UIViewController
在SwiftUI
中使用
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
}
}
SwiftUI
畫布的Resume
快捷鍵:Option + Command + P
Form
表單混稽、Section
分段嫂侍、Group
分組
SwiftUI
限制Section
和Group
包含不能超過10個,Section
可設(shè)置header
和footer
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")
}
}
- 添加導(dǎo)航欄使用
navigationBarTitle
挑宠,displayMode
設(shè)置顯示樣式
NavigationView {
Form {
Section {
Text("Hello World")
}
}
.navigationBarTitle("SwiftUI", displayMode: .inline)
}
就像
SwiftUI
的其他視圖一樣各淀,VStack
最多可以有10個子節(jié)點——如果您想添加更多子節(jié)點,應(yīng)該將它們包裝在一個Group
中诡挂。Color.red
本身就是一個視圖碎浇,這就是為什么它可以像形狀和文本一樣使用。它會自動占用所有可用空間璃俗。Color.primary
是SwiftUI
中文本的默認(rèn)顏色奴璃,根據(jù)用戶的設(shè)備是在亮模式還是在暗模式下運行,它將是黑色還是白色城豁。還有Color.secondary
苟穆,它也可以是黑色或白色,這取決于設(shè)備,但現(xiàn)在它有輕微的透明度雳旅,以便后面的一點顏色可以穿透跟磨。如果要將內(nèi)容置于安全區(qū)域之下攒盈,可以使用
edgesIgnoringSafeArea()
修飾符指定要運行到的屏幕邊緣。
Color.red.ignoresSafeArea()
Color.red.edgesIgnoringSafeArea(.all)
- 漸變色
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)
}
- 如果您發(fā)現(xiàn)圖像已被某種顏色填充,例如顯示為純藍(lán)色而不是實際圖片旅择,則可能是
SwiftUI
為它們著色以顯示它們是可點擊的柱蟀。要解決此問題昼牛,請使用renderingMode(.original)
修飾符強制SwiftUI
顯示原始圖像伶椿,而不是重新著色的版本。
Image("Image Name")
.renderingMode(.original)
-
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("點擊確定")
})
})
}
}
}
Swift
內(nèi)置形狀:
矩形Rectangle
看彼、圓角矩形RoundedRectangle
茁计、圓形Circle
逊脯、膠囊Capsule
和橢圓Ellipse
使用方法:.clipShape(Capsule())
切割匕争、描邊春缕、陰影
Image("xixi")
// 邊緣形狀
.clipShape(Circle())
// 描邊
.overlay(Circle().stroke(Color.yellow, lineWidth: 2))
// 陰影
.shadow(color: .blue, radius: 20)
-
SwiftUI
為什么使用結(jié)構(gòu)體而不是用類?
首先冯键,有一個性能因素:結(jié)構(gòu)體比類更簡單,更快。我之所以說性能因素,是因為很多人認(rèn)為這是SwiftUI
使用結(jié)構(gòu)體的主要原因啃匿,而實際上這只是更大范圍的一部分。
在UIKit
中,每個視圖都來自一個名為UIView
的類,該類具有許多屬性和方法:背景色扒秸,確定其放置方式的約束写烤,用于將其內(nèi)容呈現(xiàn)到其中的圖層等等。其中有很多拾徙,每個UIView
和UIView
子類都必須具有它們洲炊,因為繼承是這樣工作的。
視圖作為結(jié)構(gòu)體還是有很多更重要的事情:它迫使我們考慮以一種干凈的方式隔離狀態(tài)。您會發(fā)現(xiàn),類能夠自由更改其值膝但,這可能導(dǎo)致代碼混亂箩帚。
通過生成不會隨時間變化的視圖,SwiftUI
鼓勵我們轉(zhuǎn)向更具功能性的設(shè)計方法:在將數(shù)據(jù)轉(zhuǎn)換為UI
時,我們的視圖變成簡單的舍杜,惰性的東西频丘,而不是會失去控制的智能化的東西窖张。
當(dāng)您查看可以作為視圖的事物時喜庞,可以看到這一點嫉你。我們已經(jīng)使用了Color.red
和LinearGradient
作為視圖——包含很少數(shù)據(jù)的簡單類型饲梭。實際上析苫,您不能找到比使用Color.red
作為視圖的更好的主意:除了“用紅色填充我的空間”之外兜叨,它不包含任何信息。
相比之下衩侥,Apple的UIView文檔列出了UIView
擁有的約200種屬性和方法国旷,無論是否需要它們,所有這些屬性和方法都將傳遞給其子類茫死。
提示:如果您在視圖中使用類跪但,則可能會發(fā)現(xiàn)代碼無法編譯或在運行時崩潰。
SwiftUI
應(yīng)用點語法修改視圖峦萎,返回的也是視圖類型屡久。每次我們修改視圖時忆首,SwiftUI
都會使用以下泛型來應(yīng)用該修飾符:ModifiedContent<OurThing, OurModifier>
。為什么
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個夭拌。
- 自定義修飾符魔熏,使用
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))
}
}
-
LazyVGrid
和LazyHGrid
使用(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)
}
}
}
}
}
-
ForEach
使用區(qū)別:
let agents = ["Cyril", "Lana", "Pam", "Sterling"]
VStack {
ForEach(0 ..< agents.count) {
Text(self.agents[$0])
}
}
我們回到Swift
如何識別數(shù)組中的值蒜绽。當(dāng)我們使用0..<5
或0..<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
}
- 使用定制綁定
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()
}
}
-
double
轉(zhuǎn)String
保留幾位小數(shù)
// 保留2位小數(shù)
Text("\(sleepAmount, specifier: "%.2f") 小時")
// 保留2位小數(shù),并自動刪除末尾不需要的0
Text("\(sleepAmount, specifier: "%.2g") 小時")
-
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()
}
}
}
-
DateComponents
和DateFormatter
使用
// 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())
-
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
}
}
- 顯示視圖時調(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)
}
- 使用多個
.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)
- 手勢動畫
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
}
}
)
}
}
- 結(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)體可帽。
- 為什么使用
@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
脚牍。
-
@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)建我們的類的實例。
- 彈出模態(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()
}
}
}
-
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)用眉孩,重啟,然后再計算哈希值 —— 是有可能返回不同的值的勒葱。
- 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
}
}
}
}
- 多個點擊刪除
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())
}
}
}
- 本地數(shù)據(jù)存儲
UserDefaults
存
UserDefaults.standard.setValue(self.tapCount, forKey: "tapCount")
取(未設(shè)置有默認(rèn)值)
UserDefaults.standard.integer(forKey: "tapCount")
- 調(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)題"))
}
}
}
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)題"))
}
}
}
-
List + ForEach
與ScrollView + 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)題"))
}
}
}
- 布局優(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)
}
}
-
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))
}
}
-
stride
使用
從起始值以指定值步幅到結(jié)束值的序列
從0度到360度,每22.5度一步生成一個序列
stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 8)
-
stroke
繪制邊框
Circle()
.stroke(Color.blue, lineWidth: 4)
.padding(100)
- 循環(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
}
}
- 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)
}
}
}
- 啟用
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)
}
}
通過應(yīng)用一個稱為drawingGroup()
的新修改器來解決此問題荒椭。這告訴SwiftUI
谐鼎,在將視圖內(nèi)容作為單個呈現(xiàn)的輸出放回到屏幕上之前,應(yīng)將視圖的內(nèi)容呈現(xiàn)到屏幕外的圖像中(離屏渲染)趣惠,這要快得多狸棍。在幕后身害,該功能由Metal
提供支持,Metal
是Apple
的框架隔缀,可直接與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)
}
}
}
之所以命名為“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)
}
}
模糊效果
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()
}
}
}
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揪惦、使用URLSession
和URLRequest
請求數(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)
}
}
}
}
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
從全局環(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)")
}
}
}