SwiftUI - 常用Views / Modifies(二)

1、Gradient

漸變效果有四種,AngularGradient(時鐘樣式漸變)EllipticalGradient(橢圓漸變)LinearGradient(線性漸變)RadialGradient(雷射漸變),都可以直接在右上角添加視圖中找到:

//startPoint endPoint 控制漸變方向,是從上往下還是從左往右,還是左上到右下
LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue, .red]), startPoint: .top, endPoint: .bottom)
//colors 從內向外發射漸變,最后一個是背景色
//startRadius 控制第一個顏色的半徑
//endRadius 控制倒數第二個半徑,中間的顏色均勻分布漸變效果
RadialGradient(gradient: Gradient(colors: [Color.red, Color.blue, .yellow, .purple]), center: .center, startRadius: 10, endRadius: 150)

其中的.center是設置漸變從起始點(中心點)的位置,可以在代碼中試試。

2、Badge / TabView

看到Badge,就能想到是和Tabbar小圖標相關的,SwiftUI里的Badge除了可以作為底部Tabbar小圖標外,還能在List中使用,如下所示:

List(0..<100){ i in
      Text("Hello World").badge(2)
}

Badge

右側展示的就是badge,可用作未讀消息條數展示功能。另外不只是展示數字,也可以展示一些簡單的自定義Text:

List(0..<100){ i in
      Text("Hello World").badge(Text("111").font(.largeTitle)
       .foregroundColor(.orange).bold())
}

TabView就是之前的Tabbar,而之前的UITableView在SwiftUI里面被List替代了,這兩個不要搞混了。

TabView() {
       Text("Tab1").badge(3).tabItem {
             Image(systemName: "person")
             Text("Tab1")
       }.tag(1)
       Text("Tab2").tabItem {
               Image(systemName: "circle")
               Text("Tab2")
       }.tag(2)
 }

如果給TabView添加.tabViewStyle(.page)之后,TabView就變成了類似UIScrollView的左右滑動輪播圖的效果,而且還是自帶UIPageControll小圓點,如果想隱藏小圓點的話加上.page(indexDisplayMode: .never)就行了。TabView具體的使用細節以后項目中再細說...

3、OnOpenUR

用作瀏覽器跳轉到此App的鏈接,可以根據鏈接跳轉到目標界面,緊接上面的TabView:

@State var selectionIndex = 1
@State var show = false

TabView(selection: $selectionIndex) {
                Text("Tab1").badge(3).tabItem {
                    Image(systemName: "person")
                    Text("Tab1")
                }.tag(1)
                Text("Tab2").tabItem {
                    Image(systemName: "circle")
                    Text("Tab2")
                }.tag(2)
            }
            .onOpenURL { url in
                switch url.host {
                case "Tab1":
                    selectionIndex = 1
                case "Tab2":
                    selectionIndex = 2
                default:
                    show.toggle()
                    
                }
            }
            .sheet(isPresented: $show) {
                Text("參數錯誤")
            }

info里面在URL Types中添加外鏈host(Lcr):

添加URL

運行項目,打開瀏覽器輸入Lcr://Tab1即可進入App的Tab1界面,輸入Lcr://Tab2即可進入Tab2界面,如果輸入Lcr://Tab3就會跳出報錯彈窗。
拓展

  • URL 刪除的話可以在info.plist文件中刪除,然后重啟項目就看不到剛剛添加的URL Types了。
  • interactiveDismissDisab上面例子中的參數錯誤sheet為presentViewController,自帶下滑隱藏功能,如果需要關閉此功能,可以添加interactiveDismissDisab()即可:
Text("參數錯誤").interactiveDismissDisabled()
4、Animation

SwiftUI里動畫寫法很簡潔,代碼量少,此處我們以scaleEffect為示例:

@State var scaleAmount: CGFloat = 1
//scaleEffect 縮放動畫
//easeInOut 動畫方式 如深入淺出
//repeatForever 重復動畫
//onAppear 初始化為2時會自動開始動畫,如果為1是否已經動畫需要再研究,視覺上是沒有
Button("animation"){
           scaleAmount += scaleAmount <= 1 ?1 : -1
}.font(.title).padding().background(.green).cornerRadius(20)
.scaleEffect(scaleAmount).animation(Animation.easeInOut(duration: 3).repeatForever(), value: scaleAmount)
 .onAppear {
        scaleAmount = 2
  }

就能實現一個綠色按鈕放大縮小的動畫。
如果動畫效果不需要原路返回,加上.repeatForever(autoreverses: false)就可以了。

5、searchale

searchable就是我們經常用的UISearchBar+UISearchController,下面我們利用一個例子來簡單用一下:

struct ItemModel: Identifiable {
    var id = UUID()
    var name : String
    var detailView: DetailView
}

struct DetailView: View, Identifiable {
    var id = UUID()
    var detail: String
    var body: some View {
        Text(detail).font(.largeTitle).foregroundColor(.gray).bold()
    }
}

let datas: [ItemModel] = [
    ItemModel(name: "Tom", detailView: DetailView(detail: "Tom喜歡吃蘋果")),
    ItemModel(name: "Jim", detailView: DetailView(detail: "Jim喜歡吃香蕉")),
    ItemModel(name: "Lily", detailView: DetailView(detail: "Lily喜歡吃梨")),
    ItemModel(name: "Lucy", detailView: DetailView(detail: "Lucy喜歡吃橘子")),
]

class ViewModel: ObservableObject {
    @Published var allItems: [ItemModel] = datas
    @Published var searchString: String = ""
    
    var filteredItems: [ItemModel] {
        searchString.isEmpty ? allItems :
        allItems.filter({ item in
            item.name.lowercased().contains(searchString.lowercased())
        })
    }
    
}

struct ContentView: View {
    @ObservedObject var vm = ViewModel()
    var body: some View {
        NavigationView{
            List{
                ForEach(vm.filteredItems) { item in
                    NavigationLink(item.name, destination: item.detailView)
                }
            }
            .navigationTitle(Text("搜索頁面"))
            .searchable(text: $vm.searchString,prompt: "輸入你想搜索的名字")
        }
    }
}

運行效果如下,當輸入名字時篩選出適合的數據:


Searchable
6、NavigationView / NavigationLink

NavigationView導航欄,上方搜索示例中,List需要放入NavigationView中才有效,因為搜索框是和NavigationView綁定的,和之前的UISearchBar類似。

//第二界面界面
struct DetailView: View {
    var body: some View {
        //此處不用再包一層NavigationView
        VStack {
            //前往詳情界面
            NavigationLink("detail"){
                Text("Detail")
            }
        }
    }
}
//第一界面
var body: some View {
        NavigationView {
            NavigationLink("look detail"){
                DetailView()
            }
        }.navigationTitle("navi").navigationBarTitleDisplayMode(.inline)
}

NavigationLink可以理解為一種點擊可以跳轉界面的控件。
有一點讓我不理解的是,上方搜索示例中的.navigationTitle和.searchable為什么不是加在NavigationView上,而是加在List上的。在查看NavigationView源碼后,源碼中也有使用示例,我猜可能是NavigationView就是起一個包裝容器作用吧,相關的title、樣式設置放在了第一層子View上。
注意:NavigationView只需要在最外層寫一個就好,內層界面不用再寫

7、DatePicker

日期選擇器,樣式比之前的系統自帶的樣式好看多了,操作也方便:

@State var date = Date()
    
    let dateRange: ClosedRange<Date> = {
        let calender = Calendar.current
        let startComponents = DateComponents(year: 2021, month:1, day: 1)
        let endComponents = DateComponents(year: 2021, month:12, day:31, hour:23, minute:59, second:59)
        return calender.date(from: startComponents)!
        ...
        calender.date(from: endComponents)!
    }()

//selection 當前選擇的日期時間
//in  范圍,設定只能在這個范圍內選擇
//displayedComponents 模式,日期事件的模式
DatePicker(selection: $date, in: dateRange, displayedComponents: [.date, .hourAndMinute]) {
      //Text("\(date.description)")            
}
  //.frame(width: 200,height: 50)

加上選擇的范圍后,就不能選擇范圍外的日期及時間。
ClosedRange 不可數的一個范圍,SwiftUI里提供了四種表示范圍的結構:

  • CountableClosedRange : 可數的一個閉區間范圍 支持for循環
  • CountableRange :可數的一個開區間范圍 支持for循環
  • ClosedRange :不可數 不支持for 循環
  • Range :不可數 不支持for循環
8、contextMenu / Menu

一種彈窗式的選擇器,類似于微信右上角點擊?的PopView

@State var backgroundColor = Color.red
@State var isShow = true

Text("Hello Lcr").bold().font(.largeTitle).foregroundColor(.white).background(backgroundColor)
                .contextMenu(isShow ? ContextMenu{
                    Button("Red"){
                        backgroundColor = .red
                    }
                    Button("Blue"){
                        backgroundColor = .blue
                    }
                    Button("Yellow"){
                        backgroundColor = .yellow
                    }
                    Button("Green"){
                        backgroundColor = .green
                    }
                } : nil)

長按Text,會彈出選擇器,切換Text背景顏色,不妨動手試試吧。選項的顯示順序,從上往下排列。

Menu功能和樣式上也是和contextMenu一樣的,但單擊就行,不需長按,寫法也簡便點。

Menu("Menu"){
                Text("選項一")
                Text("選項二")
                Text("選項三")
                Button("Blue"){
                    backgroundColor = .blue
                }
                Button("Yellow"){
                    backgroundColor = .yellow
                }
                Button("Green"){
                    backgroundColor = .green
                }
            }.font(.largeTitle)

Menu選項的顯示順序,從下往上排列。Menu可以無限嵌套。

9、Map

地圖的使用很簡單,導入MapKit框架,把傳統的MKMapView轉化成SwiftUI里的View,那就需要自定義的MapView遵循UIViewRepresentable協議,實現相關方法:

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
        let mapView = MKMapView()
        let locationManager = CLLocationManager()
        mapView.delegate = context.coordinator
        mapView.userTrackingMode = .followWithHeading
        locationManager.requestAlwaysAuthorization()
        return mapView
    }
    func updateUIView(_ view: MKMapView, context: UIViewRepresentableContext<MapView>) {
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

class Coordinator: NSObject, MKMapViewDelegate {
    var parent: MapView
    init(_ parent: MapView) {
        self.parent = parent
    }
    func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
        print("----", mapView.centerCoordinate)
    }
    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
        mapView.centerCoordinate = userLocation.coordinate
    }
}

struct ContentView: View {
    var body: some View {
        MapView().ignoresSafeArea()
    }
}

協議中的makeUIView方法返回一個MKMapView對象。
如果需要用到地圖的相關代理方法,就要添加代理類Coordinator去遵守MKMapViewDelegate協議,然后設置代理及在makeCoordinator方法將二者綁定。

10、List

SwiftUI里List 替代了UITableView,結構上也模仿了UITableView及Cell之間的結構,使用起來效率是真的高,代碼量少:

List{
       ForEach(0..<100){
            Text("Cell \($0)").listRowSeparator(.hidden)
       }
}.font(.largeTitle).listStyle(.plain)

幾句代碼就完成了一個列表的創建。
分組用法如下:

List{
       ForEach(0..<3) { _ in
            Section {
                      ForEach(0..<6){
                          Text("Cell \($0)")
                              .listRowSeparator(.hidden)
                      }
            } header: {
                    Text("Header").foregroundColor(.white)
                          .padding().background(.blue)
                    }
        }
}.font(.largeTitle).listStyle(.grouped)

即可創建分組List,.listStyle為列表展示樣式,當設置為.listStyle(.plain)時,組頭可懸停在頂部;當設置.listStyle(.sidebar)時每一組都可以收縮及放開,很方便。
如果需要給每個“Cell”添加操作比如刪除,那就需要給"Cell"的內容添加.swipeActions():

//edge 設置按鈕的位置,是左側還是右側滑出
//allowsFullSwipe 滑動的距離長短區別
Text("Cell \($0)")
          .listRowSeparator(.hidden)
          .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                Button("delete"){
                       print("delete")
                }
          }

List默認自帶頂部刷新功能(底部加載功能暫時沒有提供),給List添加.refreshable()即可滿足一般的刷新需求,如果想要加文字圖片啥的就需要自定義了。

.refreshable {
      print("refresh")
 }

漸漸接觸到的控件也越來越多了,這些都是做好項目必經之路,打好基礎才能搭建高樓大廈,我們未完待續!!!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容