仿天貓首頁- SwiftUI版

天貓首頁效果圖

開發環境

macOS Catalina 10.15 beta 7, xcode 11.0 beta 6git

SwiftUI編寫天貓App

1、輪播圖實現

輪播圖的這裏實現是按照官網的例子來實現的,代碼在工程的下圖文件夾中:

這裏假設你已經熟悉了SwiftUI的基本寫法。

  1. 在PageView.swift中咱們的body中的代碼以下:
var body: some View {
        
        ZStack(alignment: .bottom) {
            
            /// 滑動控制器視圖
            PageViewController(currentPage: $currentPage, offsetX: $offsetX, home: self.home, controllers: viewControllers)
                .background(Color.clear)
                .frame(height: 260)
            
            Text("")
                .preference(key: PageKeyTypes.PreKey.self, value: [PageKeyTypes.PreData(index: currentPage,offsetX: offsetX)])
            
            /// 新修改頁數指示
            TMPageView().padding()
            
            
        }.onPreferenceChange(PageKeyTypes.PreKey.self) { values in
            self.home.index = values.first?.index ?? 0
        }
    }
複製代碼

1)這裏 PageViewController()是使用的UIPageViewController實現的,使用的UIKit的控制器,因此裏面要遵循UIViewControllerRepresentable這個協議。github

2)Text("") 這一行代碼主要是爲了監聽ScrollView的滾動事件,這裏咱們使用的是preference來實現。swift

3) TMPageView() 這個是頁碼指示器。xcode

1.1 PageViewController頁面安全

struct PageViewController: UIViewControllerRepresentable {
  typealias UIViewControllerType = UIPageViewController
  
  /// 當前頁
  @Binding var currentPage: Int
  
  /// 當前頁偏移量
  @Binding var offsetX: CGFloat

  /// 傳遞過來的首頁全局數據
  var home: HomeGlobal
  
  var controllers: [UIViewController]
  func makeUIViewController(context: UIViewControllerRepresentableContext<PageViewController>) -> UIPageViewController {
      let pageViewController = UIPageViewController(
          transitionStyle: .scroll,
          navigationOrientation: .horizontal,options: [:])
      pageViewController.dataSource = context.coordinator
      pageViewController.delegate = context.coordinator

        /// 獲取page內的scrollView
      let scrol = findScrollView(vc: pageViewController)
      scrol.delegate = context.coordinator
      return pageViewController
  }
  
  func updateUIViewController(_ uiViewController: UIPageViewController, context: UIViewControllerRepresentableContext<PageViewController>) {
      uiViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true, completion: nil)
  }
  
  func findScrollView(vc: UIPageViewController) -> UIScrollView {
      for item in vc.view!.subviews {
          if item is UIScrollView {
              return item as! UIScrollView
          }
      }
      return UIScrollView()
  }
  
  class Coordinator: NSObject,UIPageViewControllerDataSource,UIPageViewControllerDelegate,UIScrollViewDelegate {
      var parent: PageViewController
      var home: HomeGlobal
      init(_ pageViewController: PageViewController,home: HomeGlobal) {
          self.parent = pageViewController
          self.home = home
      }
      
      /// 數據源代理
      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]
      }
      
      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)
          {
              parent.currentPage = index
              
          }
      }
      
      /// 監聽滾動視圖距離
      func scrollViewDidScroll(_ scrollView: UIScrollView) {
          self.home.offsetX = scrollView.contentOffset.x
      }
  }
  
  func makeCoordinator() -> PageViewController.Coordinator {
      Coordinator(self, home: self.home)
  }

  
}

複製代碼

1)這裏面主要實現makeUIViewController 和 updateUIViewController,這裏面主要實現makeUIViewController用於建立UIKit框架中的控制器,updateUIViewController更新的時候會調用到。app

2)class Coordinator這個類是一個協調者,用於實現SwiftUI框架和UIKit以前的連接。 咱們使用Coordinator 來實現UIPageViewController的一些代理。框架

3)由於要監聽UIPageViewController的頁面的滾動,因此這裏咱們添加findScrollView()這個方法來獲取當前頁面的UIScrollView視圖,來監聽滑動的偏移量。oop

總結:這個頁面主要實現了UIPageViewController代理和監聽UIScrollview偏移量用來修改背景顏色的漸變效果。佈局

1.2 Text("")學習

這裏主要看下:

``` swift
// preference類型
struct PageKeyTypes {

// preference 的value 類型
struct PreData: Equatable{
    let index: Int
    let offsetX: CGFloat

}
// preference 的 key
struct PreKey: PreferenceKey {
    static var defaultValue: [PreData] = []

    static func reduce(value: inout [PreData], nextValue: () -> [PreData]) {
        value.append(contentsOf: nextValue())
    }
    typealias Value = [PreData]

}
複製代碼

}

```
複製代碼

1) preference 這裏使用它,能夠爲View設置任何事件,咱們這裏使用了PreData這個類型來監聽這個View的index和offsetX。這兩個值就能夠獲取到當前的索引和偏移量了。

2) 當發生變化的時候,就會執行這裏面onPreferenceChange,獲取以後咱們給首頁的全局配置對象設置對應的值,這樣咱們就能夠在其餘任何View中獲取咱們的這些屬性值了。

1.3 TMPageView()

struct TMPageView: View {
   
   @EnvironmentObject var home: HomeGlobal
   var body: some View {
       
       ZStack(alignment: .leading) {
           Color(red: 200/255.0, green: 200/255.0, blue: 200/255.0)
               .frame(width: 150,height: 2)
               .cornerRadius(1)
           VStack {
               Color.white
                   .frame(width: 15,height: 2)
                   .cornerRadius(2)
       
           }.offset(x: CGFloat(self.home.index)*15, y: 0 )
           
                       
       }
   }
}
複製代碼

這個視圖只是指示器做用,根據傳遞進來的全局數據來設置對應的顯示位置。

1.4 這裏是整個輪播圖的預覽View

loop.featureImage用來獲取當前輪播圖的圖片Item。設置了圖片的高度和圓角,距離頂部有一段的距離是用來設置頂部導航條的間距的。

  1. 輪播圖後面的背景視圖 TMHomeBackView

這個視圖咱們是咱們首頁的背景圖,用來顯示一個默認背景圖片,根據全局數據設置不一樣的顏色。 這裏面主要使用Image這一個,其餘的代碼都是獲取背景圖片應該設置爲何顏色的代碼邏輯。

struct TMHomeBackView: View {
    
    @EnvironmentObject var home: HomeGlobal
    
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Image("loopbg")
                .resizable()
                .frame(height: 450)
                .background(Color.init(getColor()))
            
        }
        .offset(x: 0, y: self.home.offsetY <= 0 ? self.home.offsetY : 0)
    }
    
    func getColor() ->  UIColor{
        
        /// 當前頁
        let current = self.home.index
        
        /// 獲取下一頁的索引
        var nextIndex: Int = current
        
        
        /// 滑動比例
        let progress: CGFloat = abs((self.home.offsetX - self.home.width)/self.home.width)

        /// 滑動方向
        if self.home.offsetX - self.home.width >= 0 {
            nextIndex += 1
            if nextIndex > 9 {
                nextIndex = 0
            }
            if self.home.offsetX - self.home.width == 0 {
                nextIndex = 0
            }
        } else {
            
            nextIndex -= 1
            if nextIndex < 0 {
                nextIndex = 9
            }
            if  current == 0 {
                nextIndex = 0
            }
        }
        
        /// 當前顏色
        let currentColor: (r : CGFloat, g : CGFloat, b : CGFloat)
            = getRGBWithColor(getRGB(current))
        
    
        /// 下一個顏色
        let nextColor: (r : CGFloat, g : CGFloat, b : CGFloat)
            = getRGBWithColor(getRGB(nextIndex))

        print("\(currentColor)==\(nextColor)")
        
        /// 顏色變量
        let colorDelta = (currentColor.0 - nextColor.0, currentColor.1 - nextColor.1, currentColor.2 - nextColor.2)
        
        let finalColr: UIColor = UIColor(red: (currentColor.0 - colorDelta.0*progress) / 255.0, green: (currentColor.1 - colorDelta.1*progress) / 255.0, blue: (currentColor.2 - colorDelta.2*progress) / 255.0, alpha: 1)
        
        return finalColr
    }
    
    func getRGB(_ index: Int) -> UIColor {
        let color =   UIColor(red: CGFloat(loopData[index].colors.red)/255.0, green: CGFloat(loopData[index].colors.green)/255.0, blue: CGFloat(loopData[index].colors.blue)/255.0, alpha: 1)
        return color
    }
}
複製代碼

1)咱們使用getColor方法來獲取當前和下一頁應該顯示什麼樣的顏色。這裏的顏色咱們使用的是RGB顏色來進行漸變的。

2、自定義ScrollView

由於咱們在使用中發現,SwiftUI中的ScrollView不在跟UIKit中的UIScrollView同樣有代理方法,能夠監聽ScrollView的滾動事件。咱們使用ScrollView(.vertical, showsIndicators: false)發現也只有設置橫屏豎屏滾動,和是否顯示滾動條的參數。這裏好像SwiftUI中已經沒有像UIKit中代理的一些東西了。在官網例子中也沒有找到對應的實現,官網的例子中都是很簡單的教你如何使用SwiftUI。在翻看gitHub上的一些文章後,找尋到了如何自定義實現ScrollView的滾動和若是實現下拉刷新等功能。 咱們下面實現的自定義ScrollView是根據老外寫的文章編寫的:

  1. RefreshScrollView的實現
var body: some View {
        VStack {
            ScrollView(.vertical, showsIndicators: false) {
                
                 ZStack(alignment: .top) {
                    /// 用於接收監聽的視圖
                    MovingView()
                    /// 填充傳過來的視圖
                    self.content
                }
            }
            .onPreferenceChange(RefreshableKeyTypes.PreKey.self) { values in
                
                /// 更新賦值
                self.home.offsetY = values.first?.bounds.origin.y ?? 0.0
                
                self.home.width = values.first?.bounds.size.width ?? 0.0
            }
        }
    }
複製代碼

1) RefreshScrollView中的body代碼也是很是簡單,這裏仍是主要是根據preference 和 onPreferenceChange 實現的。在前面監聽滾動的時候咱們已經使用過了。 2) 這裏新增的也就多了一個 GeometryReader 這個是用來獲取設備尺寸的,

@available(iOS 13.0, OSX 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 }

   /// The container view's bounds rectangle converted to a defined
   /// coordinate space.
   public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
}
複製代碼

看這裏是GeometryProxy的size就是獲取設置寬度和高度的。

3) self.content這裏的content就是傳遞過來的顯示的View

  1. 使用 RefreshScrollView
struct TMHomeView: View {
   
   @State private var refresh: Bool = true
   @EnvironmentObject var home: HomeGlobal
   
   
   var body: some View {
       
       /// 導航總試圖
       NavigationView {
           
           /// 總體疊加
           ZStack(alignment: .top) {
               
               /// 首頁背景視圖
               TMHomeBackView()
               
               /// 滾動視圖
               RefreshScrollView(refreshing: $refresh) {
                   HomeContentView()
               }
               
               /// 頂部導航
               HomeNaviView()

           }
               /// 背景顏色
               .background(Color(red: 245/255.0, green: 245/255.0, blue: 245/255.0))
               /// 延伸到安全區域
               .edgesIgnoringSafeArea(.top)
               .navigationBarHidden(true)
       }
       
       
   }
}
複製代碼

1) 使用的時候就很是簡單了,跟其餘系統的View使用同樣

RefreshScrollView(refreshing: $refresh) {
                  HomeContentView()
              }
複製代碼

3、 其餘View的實現

  1. 這裏就挑一個導航條的代碼實現

struct HomeNaviView: View {
  
  @EnvironmentObject var home: HomeGlobal
  @State private var name: String = ""
  
  var body: some View {
      VStack(alignment: .leading, spacing: 0) {
          
          /// 頂部安全區域
          Color.red
              .frame(height: 44)
          
          /// 底部導航欄
          HStack {
              
              Image("camera_Normal")
                  .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 5))
              /// 導航條位置
              HStack{
                  
                  Image("iconfont-search")
                      .padding(EdgeInsets(top: 7, leading: 5, bottom: 8, trailing: 5))
                  TextField("智能家居HongMeng", text: $name)
                  Image("tmas_entry_pop_icon")
                      .padding(EdgeInsets(top: 7, leading: 5, bottom: 8, trailing: 5))
                  
                  
              }
              .background(
                  Color.white
                      .cornerRadius(4)
              )
                  .frame(height: 50)
              
              Image("detail_button_cart")
                  .padding(.leading, 10)
                  .padding(.trailing, 5)
              Image("frontpage_message_btn")
                  .padding(.leading, 5)
                  .padding(.trailing, 10)
              
          }
              
          .background(Color.red)
      }
      
  }
}
複製代碼

1)使用了圖片、文本、輸入框等View的組合。

2) 其餘View的實現主要看代碼吧,寫法都是同樣的實現起來很簡單。

4、總體預覽

5、總結

  1. 使用了SwiftUI編寫程序,若是學會了,寫起來就很是的簡單了,寫的佈局UI效果立馬就會展示在眼前,不再用從新運行了。開發速度變快了不少。
  2. 和flutter相比較,flutter代碼結構更加的嵌套,修改起來還得一個個的去找在哪一個層級。SwiftUI 寫起來層級仍是看着比較簡潔點。
  3. SwiftUI只是蘋果系的跨平臺,而flutter是不少平臺了。

6、其餘疑問?

  1. SwiftUI 中還有不少不會使用的,好比: NavigationLink 跳轉到其餘View 頁面,咱們想自定義實現返回按鈕,還不知道點擊按鈕後如何pop到上一個頁面。
  2. SwiftUI好像也沒有返回系統返回手勢。

奉上上面全部的 代碼示例,以供參考,共同窗習;

相關文章
相關標籤/搜索