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