---
title: 'SwiftUI 元件使用'
tags: SwiftUI
disqus: hackmd
---
**目錄:**
[TOC]
## TabView
:::warning
由於原生 TabView + Navigation 會導致頁面被 retain 的問題,因此目前都改用 UIKit 方式實作 TabView
:::
### 封裝
```swift=
// 客製化 TabBar
class CustomTabBar: UITabBar {
override func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = 107 // 改變高度
return sizeThatFits
}
}
```
```swift=
// Step 1: 创建一个 UITabBarController 的封装
class TabViewController: UITabBarController {
let tabTypes: [TabScreen.TabType] = TabScreen.TabType.allCases
deinit {
print("deinit \(self)")
}
override func viewDidLoad() {
super.viewDidLoad()
setValue(CustomTabBar(), forKey: "tabBar")
viewControllers = tabTypes.map { UIHostingController(rootView: $0) }
}
/// 設置客製化邊線
func setupCustomTabBarBorder(theme: ThemeType) {
// 移除默认的分割线
tabBar.shadowImage = UIImage()
tabBar.backgroundImage = UIImage()
// 添加自定义分割线
let borderLayer = CALayer()
borderLayer.frame = CGRect(x: 0, y: 0, width: tabBar.frame.width, height: 2)
borderLayer.backgroundColor = UIColor(theme.associatedObject.color.colorsGrayScale100).cgColor
tabBar.layer.addSublayer(borderLayer)
// 設定 Tabbar
tabBar.items?.enumerated()
.forEach {
let tab = tabTypes[$0.offset]
$0.element.tag = tab.rawValue
$0.element.title = tab.title
$0.element.image = tab.getUnSelectImage(imageTheme: theme.imageTheme)
$0.element.selectedImage = tab.getSelectedImage(imageTheme: theme.imageTheme)
}
}
/// 設置 TabBar 隱藏
func setTabBarHidden(_ shouldShowTabBar: Bool, animated: Bool = true) {
let duration: TimeInterval = animated ? 0.3 : 0
UIView.animate(withDuration: duration) { [weak self] in
self?.tabBar.isHidden = !shouldShowTabBar
}
}
}
```swift
// Step 2: 用 UIViewControllerRepresentable 封装
struct UIKitTabView: UIViewControllerRepresentable {
let shouldShowTabBar: Bool
let theme: ThemeType
let language: LanguageType // 為了刷新語系使用(實際上這個參數無人呼叫)
func makeUIViewController(context: Context) -> TabViewController {
.init()
}
func updateUIViewController(_ uiViewController: TabViewController, context: Context) {
uiViewController.setTabBarHidden(shouldShowTabBar, animated: true)
uiViewController.setupCustomTabBarBorder(theme: theme)
}
}
```
### 使用
```swift
UIKitTabView(shouldShowTabBar: appState.shouldShowTabBar, theme: appState.theme, language: appState.language)
```
## LoadMoreView (續撈元件)
### 封裝
```swift
/// 續撈元件
struct LoadMoreView: View {
enum LoadingState: Equatable {
case loading(page: Int)
case success(nextPage: Int?)
case fail(retryPage: Int)
/// 是否已經完全加載
var isDataFullyLoaded: Bool {
self == .success(nextPage: nil)
}
/// 是否為第一次失敗
var isFristFail: Bool {
self == .fail(retryPage: serverLoadMoreInitPage)
}
}
@Binding var loadingState: LoadingState
let loadData: (_ page: Int) -> ()
var body: some View {
Group {
switch loadingState {
case .loading(let page):
ProgressView()
.onAppear {
loadData(page)
}
case .success(let nextPage?):
ProgressView()
.onAppear {
loadData(nextPage)
}
case .fail(let retryPage):
AppearEmptyView {
loadData(retryPage)
}
default:
EmptyView()
}
}
.frame(maxWidth: .infinity, minHeight: 100)
}
}
```
### 使用
```swift
// In Screen
// 續撈
LoadMoreView(loadingState: $viewModel.loadingState) { [weak viewModel] page in
// 呼叫 API 取資料
viewModel?.getActiveList(page: page)
}
```
```swift
// In ViewModel
// 續撈狀態
@Published var loadingState: LoadMoreView.LoadingState = .loading(page: serverLoadMoreInitPage)
// 續撈
self.loadingState = .success(nextPage: page)
// 撈到底
self.loadingState = .success(nextPage: nil)
// 撈取失敗
self.loadingState = .fail(retryPage: page)
```
## WKAsyncImage (帶有 Cache 的 AsyncImage)
### 封裝
```swift
extension URLSession {
// 創建與配置圖片 Cache 的 URLSession
static let imageSession: URLSession = {
let config = URLSessionConfiguration.default
config.urlCache = .imageCache
return .init(configuration: config)
}()
}
extension URLCache {
// 設定 Cache 的大小
static let imageCache: URLCache = {
.init(memoryCapacity: 20 * 1024 * 1024,
diskCapacity: 30 * 1024 * 1024)
}()
}
```
```swift
// In View
/// 帶有 Cache 的 AsyncImage
struct WKAsyncImage: View {
// 這邊若沒有用 @ObservedObject iOS 16.1 ~ 16.3 會抓到錯的圖片
@ObservedObject private var viewModel: WKAsyncImageViewModel
init(url: String, session: URLSession = .imageSession) {
self._viewModel = .init(wrappedValue: .init(url: url, session: session))
}
var body: some View {
Group {
switch viewModel.phase {
case .empty:
ProgressView()
.scaleEffect(2)
.task { await viewModel.load() }
case .success(let image):
image
.resizable()
case .failure:
EmptyView()
@unknown default:
#if DEV
fatalError("This has not been implemented.")
#else
EmptyView()
#endif
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
```
```swift
// In ViewModel
// 由於只在外面使用 @State 會導致 iOS 16.1 ~ 16.3 抓到錯的圖片
// 因此創個 ViewModel 讓外面使用 @ObservedObject 去重新 init
class WKAsyncImageViewModel: ObservableObject {
// 需要設定 @ObservedObject 讓他重新 init()
@Published var phase: AsyncImagePhase = .empty
private let session: URLSession
private let urlRequest: URLRequest?
deinit {
print("deinit: \(self)")
}
init(url: String, session: URLSession) {
self.session = session
if let url = URL(string: url) {
let urlRequest = URLRequest(url: url)
self.urlRequest = urlRequest
if let data = session.configuration.urlCache?.cachedResponse(for: urlRequest)?.data,
let uiImage = UIImage(data: data) {
phase = .success(.init(uiImage: uiImage))
}
} else {
urlRequest = nil
}
}
func load() async {
do {
guard let urlRequest else { return }
let (data, response) = try await session.data(for: urlRequest)
guard let response = response as? HTTPURLResponse,
200...299 ~= response.statusCode,
let uiImage = UIImage(data: data)
else {
throw URLError(.unknown)
}
phase = .success(.init(uiImage: uiImage))
} catch {
phase = .failure(error)
}
}
}
```
### 使用
```swift
WKAsyncImage(url: url)
```