SwiftUI官方教程解讀

SwiftUI簡(jiǎn)介

SwiftUI是wwdc2019發(fā)布的一個(gè)新的UI框架,通過聲明和修改視圖來(lái)布局UI和創(chuàng)建流暢的動(dòng)畫效果。并且我們可以通過狀態(tài)變量來(lái)進(jìn)行數(shù)據(jù)綁定實(shí)現(xiàn)一次性布局;Xcode 11 內(nèi)建了直觀的新設(shè)計(jì)工具canvus,在整個(gè)開發(fā)過程中,預(yù)覽可視化與代碼可編輯性能同時(shí)支持并交互,讓我們可以體驗(yàn)到代碼和布局同步的樂趣;同時(shí)支持和UIkit的交互.

設(shè)計(jì)工具canvus
  • 開發(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é)構(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è)控件撐開
                    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定義的根視圖開始的, UIhostingController 是UIViewController的子類

動(dòng)畫效果

SwiftUI包括帶有預(yù)定義或自定義的基本動(dòng)畫 以及彈簧和流體動(dòng)畫,可以調(diào)整動(dòng)畫速度,設(shè)置延遲,重復(fù)動(dòng)畫等等
可以通過在一個(gè)動(dòng)畫修改器后面添加另一個(gè)動(dòng)畫修改器來(lái)關(guān)閉動(dòng)畫

  • 轉(zhuǎn)場(chǎng)動(dòng)畫
    系統(tǒng)轉(zhuǎn)場(chǎng)動(dòng)畫調(diào)用: hikeDetail(hike.hike).transition(.slide)
    自定義的轉(zhuǎn)場(chǎng)動(dòng)畫:把轉(zhuǎn)場(chǎng)動(dòng)畫作為AnyTransition類的類型屬性 (方便點(diǎn)語(yǔ)法設(shè)置豐富自定義動(dòng)畫)
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)畫;move(edge:)方法是讓視圖從同一邊滑出來(lái)以及消失;asymmetric(insertion:removal:)設(shè)置出現(xiàn)和小時(shí)的不同的動(dòng)畫效果

  • 阻尼動(dòng)畫
var animation: Animation {  //定義成存儲(chǔ)屬性方便調(diào)用
        Animation.spring(initialVelocity: 5)//重力效果,值越大,彈性越大
            .speed(2)//動(dòng)畫時(shí)間,值越大動(dòng)畫速度越快
            .delay(0.03 * Double(index))
    }
  • 基礎(chǔ)動(dòng)畫
                Button(action: //點(diǎn)擊按鈕顯示一個(gè)view帶轉(zhuǎn)場(chǎng)的動(dòng)畫效果
                    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)畫效果,只顯示下面的動(dòng)畫
                       //選中的時(shí)候放大為原來(lái)的1.5倍
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                      //  .animation(.basic()) 實(shí)現(xiàn)簡(jiǎn)單的基礎(chǔ)動(dòng)畫
                        //.animation(.spring()) 阻尼動(dòng)畫
                    
                }

給圖片按鈕加動(dòng)畫效果, 對(duì)應(yīng)的會(huì)有旋轉(zhuǎn)和縮放會(huì)有動(dòng)畫;加到action時(shí),即使點(diǎn)擊完成后的顯示沒有給image的可做動(dòng)畫屬性加動(dòng)畫效果,全部都有動(dòng)畫,包含旋轉(zhuǎn)縮放和轉(zhuǎn)場(chǎng)動(dòng)畫

數(shù)據(jù)流

利用SwiftUI環(huán)境中的存儲(chǔ) ,把自定義數(shù)據(jù)對(duì)象綁定到view ,SwiftUI監(jiān)視到可綁對(duì)象任何影響視圖的更改并在更改后顯示正確的視圖

  • 自定義綁定類型
    聲明為綁定類型 BindableObject ,PassthroughSubject是Combine框架的消息發(fā)布者, SwiftUI通過這個(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里面通過didchange發(fā)布者發(fā)布更改

  • 綁定屬性
    (1)state
@State var profile = Profile.default

狀態(tài)是隨時(shí)間變化影響頁(yè)面布局內(nèi)容和行為的值
給定類型的持久值,視圖通過該持久值讀取和監(jiān)視該值。狀態(tài)實(shí)例不是值本身;它是讀取和修改值的一種方法。若要訪問狀態(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ù)的引用,用$的前綴訪問狀態(tài)變量或者其屬性之一實(shí)現(xià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í)候可以訪問coordinator對(duì)象
我們可以使用這個(gè)協(xié)調(diào)器來(lái)實(shí)現(xiàn)常見的Cocoa模式,例如委托、數(shù)據(jù)源和通過目標(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
        }
    }
}

\color{rgb(150,90,150)}{QA}: 當(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)容參見demo中profiles的部分;對(duì)于畫圖的部分demo中也有非??犰诺氖纠?,詳情參見 HikeGraphBadge(徽章)

參考資料

Apple官網(wǎng)教程 :https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
demo下載
SwiftUI documentation

作者簡(jiǎn)介

就職于甜橙金融(翼支付)信息技術(shù)部,負(fù)責(zé) iOS 客戶端開發(fā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容