SwiftUI簡(jiǎn)介
SwiftUI是wwdc2019發(fā)布的一個(gè)新的UI框架芒涡,通過(guò)聲明和修改視圖來(lái)布局UI和創(chuàng)建流暢的動(dòng)畫(huà)效果峰尝。并且我們可以通過(guò)狀態(tài)變量來(lái)進(jìn)行數(shù)據(jù)綁定實(shí)現(xiàn)一次性布局订咸;Xcode 11 內(nèi)建了直觀的新設(shè)計(jì)工具canvus,在整個(gè)開(kāi)發(fā)過(guò)程中鸳玩,預(yù)覽可視化與代碼可編輯性能同時(shí)支持并交互缘琅,讓我們可以體驗(yàn)到代碼和布局同步的樂(lè)趣;同時(shí)支持和UIkit的交互
設(shè)計(jì)工具canvus
- 開(kāi)發(fā)者可以在canvus中拖拽控件來(lái)構(gòu)建界面, 所編輯的內(nèi)容會(huì)立刻反應(yīng)到代碼上
- 切換不同的視圖文件時(shí)canvus會(huì)切換到不同的界面
- 點(diǎn)擊左下角的按鈕釘我們可以把視圖固定在活躍頁(yè)面
- 選中canvus中的控件command+click可以調(diào)出inspect布局控件的屬性
- 點(diǎn)擊右上角的+可以獲取新的控件并拖拽到對(duì)應(yīng)的位置
- 在live狀態(tài)下我們可以在canvus中調(diào)試點(diǎn)擊等可交互效果 但不能縮放視圖大小
每次修改或者增加屬性需要點(diǎn)擊resume刷新canvus
landMarkDetail布局代碼見(jiàn)布局部分
文件結(jié)構(gòu)
創(chuàng)建一個(gè)SwiftUI文件,默認(rèn)生成兩個(gè)結(jié)構(gòu)體肘迎。一個(gè)實(shí)現(xiàn)view的協(xié)議,在body屬性里描述內(nèi)容和布局甥温;一個(gè)結(jié)構(gòu)體聲明預(yù)覽的view
并進(jìn)行初始化等信息,預(yù)覽view是控制器的view時(shí)可以顯示在多個(gè)模擬器設(shè)備妓布,是控件view時(shí)可以設(shè)置frame窿侈,預(yù)覽view是提供給canvus展示的
,使用了#if DEBUG 指令,編譯器會(huì)刪除代碼,不會(huì)隨應(yīng)用程序一起發(fā)布
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
//.previewLayout(.fixed(width: 300, height: 70)) 設(shè)置view控件大小
}
.environmentObject(UserData())
}
}
#endif
布局
普通的view:將多個(gè)視圖組合并嵌入到堆棧中,這些堆棧將視圖水平秋茫、垂直或者前后組合在一起
VStack { //這里的布局實(shí)現(xiàn)的是上圖canvus中l(wèi)andMarkDetail的效果
MapView(coordinate: landmark.locationCoordinate)
.frame(height: 300)//不傳width默認(rèn)長(zhǎng)度為整個(gè)界面
CircleImage(image: landmark.image(forSize: 250))
.offset(x: 0, y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text(landmark.name)
.font(.title)
HStack(alignment: .top) {
Text(landmark.park)
.font(.subheadline)
Spacer() //將水平的兩個(gè)控件撐開(kāi)
Text(landmark.state)
.font(.subheadline)
}
}
.padding()
Spacer()
}
列表的布局:要求數(shù)據(jù)是可被標(biāo)識(shí)的
(1)唯一標(biāo)識(shí)每個(gè)元素的主鍵路徑
List(landmarkData.identified(by: \.id)) { landmark in
LandmarkRow(landmark: landmark)
}
(2)數(shù)據(jù)類型實(shí)現(xiàn)Identifiable protocol,持有一個(gè)id 屬性
struct Landmark: Hashable, Codable, Identifiable {
var id: Int //
var name: String
fileprivate var imageName: String
fileprivate var coordinates: Coordinates
var state: String
var park: String
var category: Category
}
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
} //直接傳數(shù)據(jù)源
導(dǎo)航
添加導(dǎo)航欄是將其嵌入到NavigationView中,點(diǎn)擊跳轉(zhuǎn)的控件包裝在navigationButton中,以設(shè)置到目標(biāo)視圖的換位史简。navigationBarTitle設(shè)置導(dǎo)航欄的標(biāo)題,navigationBarItems設(shè)置導(dǎo)航欄右邊的item
NavigationView {//顯示導(dǎo)航view
List {
//SwiftUI里面的類似switch的控件,可以在list中直接組合布局
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(landmarkData) { landmark in
if !self.showFavoritesOnly || landmark.isFavorite {
//跳轉(zhuǎn)到地標(biāo)詳細(xì)頁(yè)面
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))//導(dǎo)航標(biāo)題
}
}
實(shí)現(xiàn)modal出一個(gè)view
.navigationBarItems(trailing:
//點(diǎn)擊navigationBarItems modal出profileHost頁(yè)面
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.accessibility(label: Text("User Profile"))
.padding(),
destination: ProfileHost()
)
)
程序運(yùn)行是從sceneDelegate定義的根視圖開(kāi)始的圆兵, UIhostingController 是UIViewController的子類
動(dòng)畫(huà)效果
SwiftUI包括帶有預(yù)定義或自定義的基本動(dòng)畫(huà) 以及彈簧和流體動(dòng)畫(huà)跺讯,可以調(diào)整動(dòng)畫(huà)速度,設(shè)置延遲殉农,重復(fù)動(dòng)畫(huà)等等
可以通過(guò)在一個(gè)動(dòng)畫(huà)修改器后面添加另一個(gè)動(dòng)畫(huà)修改器來(lái)關(guān)閉動(dòng)畫(huà)
- 轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
系統(tǒng)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)調(diào)用:hikeDetail(hike.hike).transition(.slide)
自定義的轉(zhuǎn)場(chǎng)動(dòng)畫(huà):把轉(zhuǎn)場(chǎng)動(dòng)畫(huà)作為AnyTransition類的類型屬性 (方便點(diǎn)語(yǔ)法設(shè)置豐富自定義動(dòng)畫(huà))
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale()
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
HikeDetail(hike: hike).transition(.moveAndFade)
調(diào)用轉(zhuǎn)場(chǎng)動(dòng)畫(huà)刀脏;move(edge:)方法是讓視圖從同一邊滑出來(lái)以及消失;asymmetric(insertion:removal:)設(shè)置出現(xiàn)和小時(shí)的不同的動(dòng)畫(huà)效果
- 阻尼動(dòng)畫(huà)
var animation: Animation { //定義成存儲(chǔ)屬性方便調(diào)用
Animation.spring(initialVelocity: 5)//重力效果,值越大,彈性越大
.speed(2)//動(dòng)畫(huà)時(shí)間,值越大動(dòng)畫(huà)速度越快
.delay(0.03 * Double(index))
}
- 基礎(chǔ)動(dòng)畫(huà)
Button(action: //點(diǎn)擊按鈕顯示一個(gè)view帶轉(zhuǎn)場(chǎng)的動(dòng)畫(huà)效果
withAnimation {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
//旋轉(zhuǎn)90度
.rotationEffect(.degrees(showDetail ? 90 : 0))
//.animation(nil) //關(guān)閉前面的旋轉(zhuǎn)90度的動(dòng)畫(huà)效果超凳,只顯示下面的動(dòng)畫(huà)
//選中的時(shí)候放大為原來(lái)的1.5倍
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
// .animation(.basic()) 實(shí)現(xiàn)簡(jiǎn)單的基礎(chǔ)動(dòng)畫(huà)
//.animation(.spring()) 阻尼動(dòng)畫(huà)
}
給圖片按鈕加動(dòng)畫(huà)效果愈污, 對(duì)應(yīng)的會(huì)有旋轉(zhuǎn)和縮放會(huì)有動(dòng)畫(huà);加到action時(shí)轮傍,即使點(diǎn)擊完成后的顯示沒(méi)有給image的可做動(dòng)畫(huà)屬性加動(dòng)畫(huà)效果暂雹,全部都有動(dòng)畫(huà),包含旋轉(zhuǎn)縮放和轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
數(shù)據(jù)流
利用SwiftUI環(huán)境中的存儲(chǔ) 创夜,把自定義數(shù)據(jù)對(duì)象綁定到view 杭跪,SwiftUI監(jiān)視到可綁對(duì)象任何影響視圖的更改并在更改后顯示正確的視圖
- 自定義綁定類型
聲明為綁定類型 BindableObject ,PassthroughSubject是Combine框架的消息發(fā)布者, SwiftUI通過(guò)這個(gè)消息發(fā)布者訂閱對(duì)象,并在數(shù)據(jù)發(fā)生變化的時(shí)候更新任何需要刷新的視圖
import Combine
import SwiftUI
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
var showFavoritesOnly = false {
didSet {
didChange.send(self)
}
}
var landmarks = landmarkData {
didSet {
didChange.send(self)
}
}
}
當(dāng)客戶機(jī)需要更新數(shù)據(jù)的時(shí)候,可綁定對(duì)象通知其訂閱者
eg:當(dāng)其中一個(gè)屬性發(fā)生更改時(shí),在屬性的didset里面通過(guò)didchange發(fā)布者發(fā)布更改
- 綁定屬性
(1)state
@State var profile = Profile.default
狀態(tài)是隨時(shí)間變化影響頁(yè)面布局內(nèi)容和行為的值
給定類型的持久值驰吓,視圖通過(guò)該持久值讀取和監(jiān)視該值涧尿。狀態(tài)實(shí)例不是值本身;它是讀取和修改值的一種方法。若要訪問(wèn)狀態(tài)的基礎(chǔ)值檬贰,請(qǐng)使用其值屬性姑廉。
(2)binding
@Binding var profile: Profile//向子視圖傳遞數(shù)據(jù)
(3)environmentObject :
@EnvironmentObject var userData: UserData
存儲(chǔ)在當(dāng)前環(huán)境中的數(shù)據(jù),跨視圖傳遞
翁涤,在初始化持有對(duì)象的時(shí)候使用environmentObject(_:)賦值可以和前面的自定義綁定類型一起使用
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
- 綁定行為
是對(duì)可變狀態(tài)或數(shù)據(jù)的引用桥言,用$的前綴訪問(wèn)狀態(tài)變量或者其屬性之一實(shí)現(xiàn)綁定控件 也可以訪問(wèn)綁定屬性來(lái)實(shí)現(xiàn)綁定
與UIkit的交互
表示UIkit的view和controller 需要?jiǎng)?chuàng)建遵UIViewRepresentable或者UIViewControllerRepresentable協(xié)議
的結(jié)構(gòu)體,SwiftUI管理他們的生命周期并在需要的時(shí)候更新
實(shí)現(xiàn)協(xié)議方法:
//創(chuàng)建展示的UIViewController,調(diào)用一次
func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
//將展示的UIViewController更新到最新的版本
func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)
//創(chuàng)建協(xié)調(diào)器
func makeCoordinator() -> Self.Coordinator
在結(jié)構(gòu)體內(nèi)嵌套定義一個(gè)coordinator類迷雪。SwiftUI管理coordinator并把它提供給context ,在makeUIView(context:)之前調(diào)用這個(gè)makeCoordinator()方法創(chuàng)建協(xié)調(diào)器
,以便在配置視圖控制器的時(shí)候可以訪問(wèn)coordinator對(duì)象
我們可以使用這個(gè)協(xié)調(diào)器來(lái)實(shí)現(xiàn)常見(jiàn)的Cocoa模式虫蝶,例如委托章咧、數(shù)據(jù)源和通過(guò)目標(biāo)操作響應(yīng)用戶事件
。
這里以用UIPageViewController實(shí)現(xiàn)輪播圖為例,要注意其中的更新頁(yè)面的邏輯~
pageview作為主view能真,組合一個(gè)PageControl 和 PageViewController實(shí)現(xiàn)圖片輪播效果
PageView: @State var currentPage = 1
定義綁定屬性 赁严,$currentPage
實(shí)現(xiàn)綁定到PageViewController
PageViewController: @Binding var currentPage: Int
定義綁定屬性,在更新的方法updateUIViewController
里面綁定顯示粉铐,點(diǎn)擊pagecontrol的更新頁(yè)面時(shí)pageviewcontroller可以更新到最新的頁(yè)面
pagecontrol: @Binding var currentPage: Int
定義綁定屬性 疼约,updateUIView
綁定顯示,pageview滑動(dòng)更新頁(yè)面 pagecontrol可以更新到正確的顯示
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 1
init(_ views: [Page]) {//傳入的view用SwiftUI的controller包裝好后面?zhèn)鹘opagecontroller
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
ZStack(alignment: .bottomTrailing) {//將currentpage綁定起來(lái)了
PageViewController(controllers: viewControllers, currentPage: $currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
.padding()
//Text("Current Page: \(currentPage)").padding(.trailing,30)
}
}
}
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
//pageviewcontroller綁定currentpage顯示當(dāng)前的頁(yè)面蝙泼,pageView變化的時(shí)候程剥,page更新頁(yè)面
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
//左滑顯示控制
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
// 右滑動(dòng)顯示控制
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
//當(dāng)view滑動(dòng)停止的時(shí)候告訴pageview當(dāng)前頁(yè)面的index(數(shù)據(jù)變化 pageview更新pagecontrol的展示)
parent.currentPage = index
}
}
}
}
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
return control
}
func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}
class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
@objc
func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
}
: 當(dāng)我們編輯一部分用戶數(shù)據(jù)的時(shí)候,我們不希望在編輯數(shù)據(jù)完成的時(shí)候影響到其他的頁(yè)面 那么我們需要?jiǎng)?chuàng)建一個(gè)副本數(shù)據(jù)汤踏, 當(dāng)副本數(shù)據(jù)編輯完成的時(shí)候 用副本數(shù)據(jù)更新真正的數(shù)據(jù)织鲸, 使相關(guān)的頁(yè)面變化 這部分的內(nèi)容參見(jiàn)demo中profiles的部分舔腾;對(duì)于畫(huà)圖的部分demo中也有非常酷炫的示例搂擦,詳情參見(jiàn)
HikeGraph
稳诚、Badge
(徽章)
參考資料
Apple官網(wǎng)教程 :https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
demo下載
SwiftUI documentation
作者簡(jiǎn)介
就職于甜橙金融(翼支付)信息技術(shù)部,負(fù)責(zé) iOS 客戶端開(kāi)發(fā)