前言
上一篇我們總結的主要是VStack裡面的東西,由他延伸到 @ViewBuilder, 接著我們上一篇總結的我們這篇內容主要說的是下面的幾點,在這些東西說完後我準備解析一下蘋果在SiwftUI文件中說道的比較好玩的一個東西,具體的我們後面在看。這篇我們還是說我們關於SwiftUI的東西,再提一下Demo程式碼我已經提交上Git了,目前Demo進度為一級頁面基本上結束,地圖點選大頭針的新增也剛處理完,程式碼有需要的小夥伴可以去Git看看,專案地址
1、View之間的跳轉(這裡有個疑問需要幫忙!)
2、稍微複雜點View的佈局思路和一些細節知識
3、SwiftUI迴圈輪播圖
這次總結的首頁的UI佈局如下,我們下面一點點的解析:
介面跳轉的問題
正常的介面跳轉邏輯實現是比較簡單的,我們先看看這個很簡單的正常跳轉,再說說我們的問題:
NavigationView{ VStack{ List{ /// 開關按鈕 /// Toggle(isOn: $userData.showFavoritesOnly) {Text("Favorites only")} ForEach(landmarkData) { landmark in if !self.userData.showFavoritesOnly || landmark.isFavorite { NavigationLink(destination:LandmarkDetail(landmark:landmark) .environmentObject(self.userData),label:{ LandmarkRow(landmark: landmark) }) } } } .listStyle(PlainListStyle()) .navigationTitle("iPhone") } }
這是一個很普通的通過 NavigationView + NavigationLink 的介面跳轉,在蘋果給的 SwiftUI 的使用例子中就是這樣寫的,當然我們在正常的使用中這樣寫也沒啥問題,那我們介面跳轉的問題是什麼呢?
如果你看了我們 Demo中的程式碼,你就知道我們是採用 TabView 巢狀 NavigationView 的形式,在這樣的模式下似乎是存在問題的, 在 TabView+NavigationView 中你利用 NavigationLink 單擊沒法跳轉,只有長按的時候才能跳轉,這個問題丟擲來,有懂得小夥伴希望能給我說一下,這個問題我也一直沒有解決!具體的我們Demo中可以看看“我的”頁面那個 List 的程式碼,問題就在那裡。要理解這點的麻煩也給我說說,感謝!
首頁佈局
我們把首頁這個佈局給解析一下,大概分了下面幾部分,我們再具體的說說:
我們看看最底層的程式碼先:
NavigationView{ ScrollView(showsIndicators:false,content: { /// Banner檢視 HomeBannerView() .environmentObject(homeViewModel) /// 服務列表 HomeServiceCircleView().frame( width: homeViewModel.homeServiceCircleWidth, height: homeViewModel.homeServiceCircleHeight) .environmentObject(homeViewModel) .offset(y: -5) /// 滾動頭條 HomeCircleNewsView().frame( width: homeViewModel.homeNewsCircleWidth, height: homeViewModel.homeNewsCircleHeight) .environmentObject(homeViewModel) /// 四個按鈕 HomeButtonView().frame( width: homeViewModel.homeButtonViewWidth, height: homeViewModel.homeButtonViewHeight) .offset(y: -5) /// 服務列 HomeServiceListView().frame( width: homeViewModel.homeServiceViewWidth, height: homeViewModel.homeServiceViewHeight) .environmentObject(homeViewModel) /// 最美的風景 HomeSnapshotView().environmentObject(homeViewModel) }).navigationTitle(title) }
這部分的程式碼沒有啥特別需要說明的,都比較簡單,可能是就是這個 environmentObject (我把它稱為環境變數)這個是需要特別說明的一個變數,從名字上可以看出,這個修飾符是針對全域性環境的。通過它我們可以避免在初始 View 時建立 ObservableObject, 而是從環境中獲取 ObservableObject,像 @EnvironmentObject,@ObservedObject,@Binding 和 @States 這幾個關鍵字還是需要需要我們特別理解的。下面這篇我們部落格園的同行總結的還是很精闢的。傳送門在這
下面是我們值得細說的一些點:
1、值得注意的 TabView + PageTabViewStyle
這是在iOS14中新出的一個值得我們注意的點,PageTabViewStyle 是14.0的新東西,但它的確能達到一個滿意的翻頁效果。和我們UIKit中的效果一樣。具體的程式碼如下:
TabView(selection: $selection) { /// 裡面的具體內容,我們寫了三頁 ForEach(0..<3){ HomeServicePageView(pageIndex: $0) .tag($0) .environmentObject(homeViewModel) } } /// PageTabViewStyle 14.0的新東西 .tabViewStyle(PageTabViewStyle()) .animation(.spring())
2、GeometryReader 它其實是有必要好好了解一下的。GeometryReader 的主要作用就是能夠獲取到父View建議的尺寸,這就是它的主要作用,要沒有它我們面臨的可能就是無休止的傳值了,SwiftUI 既然是宣告式的UI,按我的理解你就沒有辦法去獲取某一個檢視的父檢視之類的。不然怎麼體現宣告這個點呢!
這個GeometryReader在前面第一期的時候我說過這個屬性。
/// A proxy for access to the size and coordinate space (for anchor resolution) /// of the container view. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct GeometryProxy { /// The size of the container view. public var size: CGSize { get } /// Resolves the value of `anchor` to the container view. public subscript<T>(anchor: Anchor<T>) -> T { get } /// The safe area inset of the container view. public var safeAreaInsets: EdgeInsets { get } /// Returns the container view's bounds rectangle, converted to a defined /// coordinate space. public func frame(in coordinateSpace: CoordinateSpace) -> CGRect }
* size 比較直觀,就是返回父View建議的尺寸
** subscript 可以讓我們獲取.leading,.top等等類似這樣的資料
*** safeAreaInsets 可以獲取安全區域的Insets
**** frame(in:) 要求傳入一個CoordinateSpace型別的引數,也就是座標空間,可以是.local, .global 或者 .named(),其中 .named()可以自定義座標空間。
有一個還得說明一下,GeometryReader 改變了它顯示內容的方式。在 iOS 13.5 中,內容放置方式為 .center。在 iOS 14.0 中則為:.topLeading。
3、再提一點關於上面說的滾動檢視,在UIKit中我們可以用UICollectionView搞定一切,但是在SwiftUI中沒有這個控制元件,我建議採用的方式是 ScrollView + HStack + VStack 的方式去實現,很多同行有說目前來看SwiftUI的List在資料量大的情況下效能不是特別好,採用ScrollView是個不錯的方式,而且也很容易構建出來,並不是說每一個Item的位置都需要你去計算,所以沒啥可以擔心的。
除了這個List,還要一個From我們也可以瞭解下,他們倆肉眼可見的區別 在選中這個點上的區別。
迴圈輪播實現
總結一下迴圈輪播怎麼實現,採用的方案就是 HStack + Gesture + Timer 的方式,這三者就能實現一個自動迴圈滾動或者手動滾動的輪播。然後縮放的方式還是比較簡單的,我們採用改變下Image的frame的方式。
HStack 這沒啥可以具體說的,可以看程式碼,註釋比較多,就不在這裡累贅了。
Gesture 這個我們可以說說,它就是我們具體手勢的父類,像我們的單擊手勢和我們這裡用到的拖拽手勢一樣。具體的我們會看下面的程式碼,他們的區別就是像拖拽我們可以監控它的改變狀態,點選或者雙擊、長按等我們可以新增事件等等。下面是拖拽的程式碼:
/// 定義拖拽手勢 private var dragGesture: some Gesture{ DragGesture() /// 拖動改變 .onChanged { isAnimation = true dragOffset = $0.translation.width } /// 結束 .onEnded { dragOffset = .zero /// 拖動右滑,偏移量增加,顯示 index 減少 if $0.translation.width > 50{ currentIndex -= 1 } /// 拖動左滑,偏移量減少,顯示 index 增加 if $0.translation.width < -50{ currentIndex += 1 } /// 防止越界 currentIndex = max(min(currentIndex, homeViewModel.homeBannerCount() - 1), 0) } }
再看看Timer,SwiftUI區別於我們UIKit的建立方式,SwiftUI對它進行了簡化,具體的建立如下:
/// SwiftUI對定時器的簡化,可以進去看看具體引數的定義 private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
它不像我們UIKit的需要我們繫結事件,那它的事件是怎麼處理的呢?看看下面的程式碼:
/// 對定時器的監聽 .onReceive(timer, perform: { _ in currentIndex += 1 }
它的事件就是通過 onReceive 監聽處理的,所有通過 publish 建立的都是可以通過 onReceive 監聽的。那還有啥事通過 publish 建立的呢?我所用到的就是 NotificationCenter。
這樣基本上迴圈輪播的實現我們基本上都說清楚了,具體裡面的一些實現細節程式碼註釋寫的清清楚楚,還是仔細看看程式碼結合裡面的註釋來看,難度不是很大。首頁頂部自動迴圈輪播的程式碼實現如下,程式碼裡有些註釋還是比較重要的,注意看註釋:
struct HomeBannerView: View { @EnvironmentObject var homeViewModel: HomeViewModel /// SwiftUI 對定時器的簡化,可以進去看看具體引數的定義 private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() /// 拖拽的偏移量 @State var dragOffset: CGFloat = .zero /// 當前顯示的位置索引, /// 這是實際資料中的1就是資料沒有被處理之前的0位置的圖片 /// 所以這裡預設從1開始 @State var currentIndex: Int = 1 /// 是否需要動畫 @State var isAnimation: Bool = true let spacing: CGFloat = 10 var body: some View { /// 單個子檢視偏移量 = 單個檢視寬度 + 檢視的間距 let currentOffset = CGFloat(currentIndex) * (homeViewModel.homeBannerWidth + spacing) /// GeometryReader 改變了它顯示內容的方式。在 iOS 13.5 中,內容放置方式為 .center。在 iOS 14.0 中則為:.topLeading GeometryReader(content: { geometry in HStack(spacing: spacing){ ForEach(0..<homeViewModel.homeBannerCount()){ /* 如果想自定義Image大小,可以新增frame clipped()相當於UIKit裡的clipsToBounds, 與aspectRatio(contentMode: .fill)搭配使用。 注意:frame 要放在resizable後面,否則報錯, 如果需求裁剪,需要放在aspectRatio後面, clipped()前面,否則frame失效 */ Image(homeViewModel.bannerImage($0)).resizable() /// 自己嘗試一下.fill和.fit .aspectRatio(contentMode: .fill) .frame(width: geometry.size.width, height: $0 == currentIndex ? geometry.size.height:geometry.size.height*0.8 ) .clipped() /// 裁減 .cornerRadius(10) } }.frame(width:geometry.size.width, height:geometry.size.height,alignment:.leading) .offset(x: dragOffset - currentOffset) .gesture(dragGesture) /// 繫結是否需要動畫 .animation(isAnimation ?.spring():.none) /// 監聽當前索引的變化,最開始初始化為0是不監聽的, .onChange(of: currentIndex, perform: { value in isAnimation = true /// 第一張的時候 if value == 0 { isAnimation.toggle() currentIndex = homeViewModel.homeBannerCount() - 2 /// 最後一張的時候currentIndex設定為1關閉動畫 }else if value == homeViewModel.homeBannerCount() - 1 { isAnimation.toggle() currentIndex = 1 } }) /// 對定時器的監聽 .onReceive(timer, perform: { _ in currentIndex += 1 }) }).frame(width: homeViewModel.homeBannerWidth, height: homeViewModel.homeBannerHeight) } } // MARK: - extension HomeBannerView{ /// 定義拖拽手勢 private var dragGesture: some Gesture{ DragGesture() /// 拖動改變 .onChanged { isAnimation = true dragOffset = $0.translation.width } /// 結束 .onEnded { dragOffset = .zero /// 拖動右滑,偏移量增加,顯示 index 減少 if $0.translation.width > 50{ currentIndex -= 1 } /// 拖動左滑,偏移量減少,顯示 index 增加 if $0.translation.width < -50{ currentIndex += 1 } /// 防止越界 currentIndex = max(min(currentIndex, homeViewModel.homeBannerCount() - 1), 0) } } }
參考文章:
理解SwiftUI關鍵字 State Binding ObservesOgiect EnvironmentObje