---
# System prepended metadata

title: SwiftUI 元件使用
tags: [SwiftUI]

---

---
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)
```