Archive/패캠(초격차)

part4 (ch1). MyAssets 코드리뷰(feat. SwiftUI)

lgvv 2022. 2. 23. 15:31

MyAssets 코드리뷰(feat. SwiftUI)

 

 

✅ 공부를 했으니 코드 리뷰도 진행해보자. 

SwiftUI를 보면서 든 생각인데, Obj-c가 swift로 넘어왔고, 언젠가는 SwiftUI로 전부 다 넘어가지 않을까 싶다.

우선 첫번째로, 선언형 프로그래밍 방식이 UI구성이 엄청나게 간단하고, 개발 속도가 엄청나게 빨라질 것 같다는 생각이 들었다.

 

SwiftUI를 Tutorial로 더 공부하고나서 코드 리뷰를 더 자세히 해보자~!

 

✅ 폴더의 구조

폴더의 구조

 

✅ ContentView.swift

//
//  ContentView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/22.
//

import SwiftUI

struct ContentView: View {
    @State private var selection: Tab = .asset
    
    enum Tab {
        case asset
        case recommend
        case alert
        case setting
    }
    
    var body: some View {
        TabView(selection: $selection) { // 하단 탭바 아이템
            AssetView()
                .tabItem {
                    Image(systemName: "dollarsign.circle.fill")
                    Text("자산")
                }
                .tag(Tab.asset) // 태그를 달아준다.
            Color.blue
                .edgesIgnoringSafeArea(.all)
                .tabItem {
                    Image(systemName: "hand.thumbsup.fill")
                    Text("추천")
                }
                .tag(Tab.recommend)
            Color.red
                .edgesIgnoringSafeArea(.all)
                .tabItem {
                    Image(systemName: "bell.fill")
                    Text("알림")
                }
                .tag(Tab.alert)
            Color.yellow
                .edgesIgnoringSafeArea(.all)
                .tabItem {
                    Image(systemName: "gearshape.fill")
                    Text("설정")
                }
                .tag(Tab.setting)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

 

 Asset 폴더

🟠 AssetView.swift

//
//  AssetView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetView: View {
    var body: some View {
        NavigationView { // 네비게이션 view
            ScrollView { // 스유에서는 스크롤을 직접 달아줘야 한다.
                VStack(spacing: 30) { // 각 컴포넌트간 spacing
                    Spacer()
                    AssetMenuGridView()
                    AssetBannerView()
                        .aspectRatio(5/2, contentMode: .fit)
                    AssetSummaryView()
                        .environmentObject(AssetSummaryData())
                }
            }
            .background(Color.gray.opacity(0.2))
            .navigaionBarWithButtonStyle("내 자산")
        }
    }
}

struct AssetView_Previews: PreviewProvider {
    static var previews: some View {
        AssetView()
    }
}

 

🟠 AssetMenu.swift

//
//  AssetMenu.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/22.
//

import Foundation

class Asset: Identifiable, ObservableObject, Decodable {
    let id: Int
    let type: AssetMenu
    let data: [AssetData]
    
    init(id: Int, type: AssetMenu, data: [AssetData]) {
        self.id = id
        self.type = type
        self.data = data
    }
}

class AssetData: Identifiable, ObservableObject, Decodable {
    let id: Int
    let title: String
    let amount: String
    let creditCardAmounts: [CreditCardAmounts]?
    
    init(id: Int, title: String, amount: String, creditCardAmounts: [CreditCardAmounts]? = nil) {
        self.id = id
        self.title = title
        self.amount = amount
        self.creditCardAmounts = creditCardAmounts
    }
}

enum CreditCardAmounts: Identifiable, Decodable {
    case previousMonth(amount: String)
    case currentMonth(amount: String)
    case nextMonth(amount: String)
    
    var id: Int  {
        switch self {
        case .previousMonth:
            return 0
        case .currentMonth:
            return 1
        case .nextMonth:
            return 2
        }
    }
    
    var amount: String {
        switch self {
        case .previousMonth(let amount),
             .currentMonth(let amount),
             .nextMonth(let amount):
            return amount
        }
    }
    
    enum CodingKeys: String, CodingKey {
        case previousMonth
        case currentMonth
        case nextMonth
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        
        if let value = try? values.decode(String.self, forKey: .previousMonth) {
            self = .previousMonth(amount: value)
            return
        }
        
        if let value = try? values.decode(String.self, forKey: .currentMonth) {
            self = .previousMonth(amount: value)
            return
        }
        
        if let value = try? values.decode(String.self, forKey: .nextMonth) {
            self = .previousMonth(amount: value)
            return
        }
        
        throw fatalError("ERROR: CreditCardAmount JSON Decoding")
    }
}

enum AssetMenu: String, Identifiable, Decodable {
    case creditScore
    case bankAccount
    case investment
    case loan
    case insurance
    case creditCard
    case cash
    case realEstate
    
    var id: String {
        return self.rawValue
    }
    
    var systemImageName: String {
        switch self {
        case .creditScore:
            return "number.circle"
        case .bankAccount:
            return "banknote"
        case .investment:
            return "bitcoinsign.circle"
        case .loan:
            return "hand.wave"
        case .insurance:
            return "lock.shield"
        case .creditCard:
            return "creditcard"
        case .cash:
            return "dollarsign.circle"
        case .realEstate:
            return "house.fill"
        }
    }
    
    var title: String {
        switch self {
        case .creditScore:
            return "신용점수"
        case .bankAccount:
            return "계좌"
        case .investment:
            return "투자"
        case .loan:
            return "대출"
        case .insurance:
            return "보험"
        case .creditCard:
            return "카드"
        case .cash:
            return "현금영수증"
        case .realEstate:
            return "부동산"
        }
    }
}

 

✅ Subcomponents 폴더

✅ NavigationBar 폴더

🟠 NavigationVarWithButton.swift

//
//  NavigationVarWithButton.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/22.
//

import SwiftUI

struct NavigationBarWithButton: ViewModifier {
    var title: String = ""
    
    func body(content: Content) -> some View {
        return content
            .navigationBarItems(
                leading: Text(title)
                    .font(.system(size: 24, weight: .bold))
                    .padding(),
                trailing: Button(
                    action: {
                        print("자산추가버튼 tapped")
                    },
                    label: {
                        Image(systemName: "plus")
                        Text("자산추가")
                            .font(.system(size: 12))
                    }
                )
                .accentColor(.black)
                .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
                .overlay(
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.black)
                )
            )
            .navigationBarTitleDisplayMode(.inline)
            .onAppear {
                let appearance = UINavigationBarAppearance()
                appearance.configureWithTransparentBackground()
                appearance.backgroundColor =
                    UIColor(white: 1, alpha: 0.6)
                UINavigationBar.appearance().standardAppearance = appearance
                UINavigationBar.appearance().compactAppearance = appearance
                UINavigationBar.appearance().scrollEdgeAppearance = appearance
            }
    }
}

extension View {
    func navigaionBarWithButtonStyle(_ title: String) -> some View {
        return self.modifier(NavigationBarWithButton(title: title))
    }
}

struct NavigationBarWithButton_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            Color.gray.edgesIgnoringSafeArea(.all)
                .navigaionBarWithButtonStyle("내 자산")
        }
    }
}

 

✅ MenuGridView.swift

🟠 AssetMenuGridView.swift

 

//
//  AssetMenuGridView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetMenuGridView: View {
    let menuList: [[AssetMenu]] = [
        [.creditScore, .bankAccount, .investment, .loan],
        [.insurance, .creditCard, .cash, .realEstate]
    ]
    
    var body: some View {
        VStack(spacing: 20) {
            ForEach(menuList, id: \.self) { row in
                HStack(spacing: 10) {
                    ForEach(row) { menu in
                        Button("") {
                            print("\(menu.title)버튼 tapped")
                        }
                        .buttonStyle(AssetMenuButtonStyle(menu: menu))
                    }
                }
            }
        }
    }
}

struct AssetMenuGridView_Previews: PreviewProvider {
    static var previews: some View {
        AssetMenuGridView()
    }
}

 

🟠 AssetMenuButtonStyle.swift

//
//  AssetMenuButtonStyle.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/22.
//

import SwiftUI

struct AssetMenuButtonStyle: ButtonStyle {
    let menu: AssetMenu
    
    func makeBody(configuration: Configuration) -> some View {
        return VStack {
            Image(systemName: menu.systemImageName)
                .resizable()
                .frame(width: 30, height: 30)
                .padding([.leading, .trailing], 10)
            Text(menu.title)
                .font(.system(size: 12, weight: .bold))
        }
        .padding()
        .foregroundColor(.blue)
        .background(Color.white)
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}

struct AssetMenuButtonStyle_Previews: PreviewProvider {
    static var previews: some View {
        HStack(spacing: 24) {
            Button("") {
                print("")
            }
            .buttonStyle(AssetMenuButtonStyle(menu: .creditScore))
            
            Button("") {
                print("")
            }
            .buttonStyle(AssetMenuButtonStyle(menu: .bankAccount))
            
            Button("") {
                print("")
            }
            .buttonStyle(AssetMenuButtonStyle(menu: .creditCard))
            
            Button("") {
                print("")
            }
            .buttonStyle(AssetMenuButtonStyle(menu: .cash))
        }
        .background(Color.gray.opacity(0.2))
    }
}

 

🟠 AssetBannerView.swift

//
//  AssetBannerView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetBannerView: View {
    let bannerList: [AssetBanner] = [
        AssetBanner(title: "공지사항", description: "추가된 공지사항을 확인하세요", backgroundColor: .red),
        AssetBanner(title: "주말 이벤트", description: "주말 이벤트 놓치지 마세요", backgroundColor: .yellow),
        AssetBanner(title: "깜짝 이벤트", description: "엄청난 이벤트에 놀라지 마세요", backgroundColor: .blue),
        AssetBanner(title: "가을 프로모션", description: "가을...🍂", backgroundColor: .brown)
    ]
    @State private var currentPage = 0
    
    var body: some View {
        let pages = bannerList.map { BannerCard(banner: $0) }

        ZStack(alignment: .bottomTrailing) {
            PageViewController(pages: pages, currentPage: $currentPage)
            PageControl(numberOfPages: bannerList.count, currentPage: $currentPage)
                .frame(width: CGFloat(pages.count * 18))
                .padding(.trailing)
        }
    }
}

struct AssetBannerView_Previews: PreviewProvider {
    static var previews: some View {
        AssetBannerView()
            .aspectRatio(5/2, contentMode: .fit)
    }
}

 

🟠 AssetBanner.swift

//
//  AssetBanner.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import UIKit

struct AssetBanner {
    let title: String
    let description: String
    let backgroundColor: UIColor
}

 

🟠 BannerCard.swift

//
//  BannerCard.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct BannerCard: View {
    var banner: AssetBanner
    
    var body: some View {
        Color(banner.backgroundColor)
            .overlay(
                VStack {
                    Text(banner.title)
                        .font(.title)
                    Text(banner.description)
                        .font(.subheadline)
                }
            )
    }
}

struct BannerCard_Previews: PreviewProvider {
    static var previews: some View {
        BannerCard(banner: AssetBanner(title: "공지사항", description: "추가된 공지사항을 확인하세요", backgroundColor: .red))
            .aspectRatio(5/2, contentMode: .fit)
    }
}

 

🟠 AssetSummaryView.swift

//
//  AssetSummaryView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetSummaryView: View {
    @EnvironmentObject var assetData: AssetSummaryData
    
    var assets: [Asset] {
        return assetData.assets
    }
    
    var body: some View {
        VStack(spacing: 20) {
            ForEach(assets) { asset in
                switch asset.type {
                case .creditCard:
                    AssetCardSectionView(asset: asset)
                        .frame(idealHeight: 250)
                default:
                    AssetSectionView(assetSection: asset)
                }
            }
            .background(Color.white)
            .clipShape(RoundedRectangle(cornerRadius: 10))
            .padding()
        }
    }
}

struct AssetSummaryView_Previews: PreviewProvider {
    static var previews: some View {
        ScrollView {
            AssetSummaryView()
                .environmentObject(AssetSummaryData())
                .background(Color.gray.opacity(0.2))
        }
    }
}

 

🟠 AssetSummaryData.swift

//
//  AssetSummaryData.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

class AssetSummaryData: ObservableObject {
    @Published var assets: [Asset] = load("assets.json")
}

func load<T: Decodable>(_ filename: String) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }

    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }

    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

 

🟠 assets.json

[
    {
        "id": 0,
        "type": "creditScore",
        "data": [
            {
                "id": 0,
                "title": "신용점수",
                "amount": "999점"
            }
        ]
    },
    {
        "id": 1,
        "type": "bankAccount",
        "data": [
            {
                "id": 0,
                "title": "신한은행",
                "amount": "42,000원"
            },
            {
                "id": 1,
                "title": "국민은행",
                "amount": "9,263,000원"
            },
            {
                "id": 2,
                "title": "카카오뱅크",
                "amount": "2,255,900원"
            }
        ]
    },
    {
        "id": 2,
        "type": "investment",
        "data": [
            {
                "id": 0,
                "title": "카카오페이",
                "amount": "5,003,370원"
            },
            {
                "id": 1,
                "title": "한국투자",
                "amount": "5,675,236원"
            }
        ]
    },
    {
        "id": 3,
        "type": "loan",
        "data": [
            {
                "id": 0,
                "title": "카카오뱅크",
                "amount": "-67,333,000원"
            },
            {
                "id": 1,
                "title": "하나은행",
                "amount": "-4,000,000,000원"
            }
        ]
    },
    {
        "id": 4,
        "type": "insurance",
        "data": [
            {
                "id": 0,
                "title": "삼성화재",
                "amount": "월 20,000원"
            },
            {
                "id": 1,
                "title": "한화손해보험",
                "amount": "월 77,400원"
            },
            {
                "id": 2,
                "title": "메리츠화재보험",
                "amount": "월 10,000원"
            },
            {
                "id": 3,
                "title": "롯데손해보험",
                "amount": "월 84,900원"
            }
        ]
    },
    {
        "id": 5,
        "type": "creditCard",
        "data": [
            {
                "id": 0,
                "title": "현대카드",
                "amount": "0원",
                "creditCardAmounts": [
                    {
                        "previousMonth": "10,000원"
                    },
                    {
                        "currentMonth": "45,000원"
                        
                    },
                    {
                        "nextMonth": "100,400원"
                    }
                ]
            },
            {
                "id": 1,
                "title": "우리카드",
                "amount": "9,000원",
                "creditCardAmounts": [
                    {
                        "previousMonth": "40,000원"
                    },
                    {
                        "currentMonth": "95,000원"
                        
                    },
                    {
                        "nextMonth": "2,150,400원"
                    }
                ]
            }
        ]
    },
    {
        "id": 6,
        "type": "cash",
        "data": [
            {
                "id": 0,
                "title": "이번 달 사용금액",
                "amount": "472,890원"
            }
        ]
    },
    {
        "id": 7,
        "type": "realEstate",
        "data": [
            {
                "id": 0,
                "title": "한강현대아파트",
                "amount": "16억 9천만원"
            },
            {
                "id": 1,
                "title": "여의도오피스텔",
                "amount": "2억 9천만원"
            }
        ]
    }
]

 

🟠 AssetSectionViewHeaderView.swift

//
//  AssetSectionViewHeaderView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetSectionHeaderView: View {
    let title: String
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.system(size: 20, weight: .bold))
                .foregroundColor(.accentColor)
            Divider()
                .frame(height: 2)
                .background(Color.primary)
                .foregroundColor(.accentColor)
        }
    }
}

struct AssetSectionHeaderView_Previews: PreviewProvider {
    static var previews: some View {
        AssetSectionHeaderView(title: "은행")
    }
}

 

 

🟠 SwiftUIView.swift

//
//  SwiftUIView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetCardSectionView: View {
    @State private var selectedTab = 1
    @ObservedObject var asset: Asset
    
    var assetData: [AssetData] {
        return asset.data
    }
    
    var body: some View {
        VStack {
            AssetSectionHeaderView(title: asset.type.title)
            TabMenuView(
                tabs: ["지난달 결제", "이번달 결제", "다음달 결제"],
                selectedTab: $selectedTab,
                updated: .constant(.nextMonth(amount: "10000"))
            )
            TabView(
                selection: $selectedTab,
                content: {
                    ForEach(0...2,  id: \.self) { i in
                        VStack {
                            ForEach(asset.data) { data in
                                HStack {
                                    Text(data.title)
                                        .font(.title3)
                                        .foregroundColor(.secondary)
                                    Spacer()
                                    Text(data.creditCardAmounts![i].amount)
                                        .font(.title2)
                                        .foregroundColor(.primary)
                                }
                                Divider()
                            }
                        }
                        .tag(i)
                    }
                }
            )
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
        }
        .padding()
    }
}

struct AssetCardSectionView_Previews: PreviewProvider {
    static var previews: some View {
        let asset = AssetSummaryData().assets[5]
        AssetCardSectionView(asset: asset)
    }
}

 

🟠 AssetMenuList.swift

//
//  AssetMenuList.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct TabMenuView: View {
    var tabs: [String]
    @Binding var selectedTab: Int
    @Binding var updated: CreditCardAmounts?
    
    var body: some View {
        HStack {
            ForEach(0..<tabs.count, id: \.self) { row in
                Button(
                    action: {
                        selectedTab = row
                        UserDefaults.standard.set(true, forKey: "creditcard_update_checked: \(row)")
                    },
                    label: {
                        VStack(spacing: 0) {
                            HStack {
                                if updated?.id == row {
                                    let checked = UserDefaults.standard.bool(forKey: "creditcard_update_checked: \(row)")
                                Circle()
                                    .fill(
                                        !checked
                                            ? Color.red
                                            : Color.clear
                                    )
                                    .frame(width: 6, height: 6)
                                    .offset(x: 2, y: -8)
                                } else {
                                    Circle()
                                        .fill(Color.clear)
                                        .frame(width: 6, height: 6)
                                        .offset(x: 2, y: -8)
                                }
                                
                                Text(tabs[row])
                                    .font(.system(size: 20, weight: .bold))
                                    .foregroundColor(
                                        selectedTab == row
                                            ? .accentColor
                                            : .gray
                                    )
                                    .offset(x: -4, y: 0)
                            }
                            .frame(height: 52)
                            Rectangle()
                                .fill(selectedTab == row
                                        ? Color.secondary
                                        : Color.clear)
                                .frame(height: 3)
                                .offset(x: 4, y: 0)
                        }
                    })
                    .buttonStyle(PlainButtonStyle())
            }
        }
        .frame(height: 55)
    }
}

struct TabMenuView_Previews: PreviewProvider {
    static var previews: some View {
        TabMenuView(tabs: ["지난달 결제", "이번달 결제", "다음달 결제"], selectedTab: .constant(1), updated: .constant(.currentMonth(amount: "10,000원")))
    }
}

 

🟠 AssetSectionView.swift

//
//  AssetSectionView.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI

struct AssetSectionView: View {
    @ObservedObject var assetSection: Asset
    var body: some View {
        VStack(spacing: 20) {
            AssetSectionHeaderView(title: assetSection.type.title)
            ForEach(assetSection.data) { asset in
                HStack {
                    Text(asset.title)
                        .font(.title3)
                        .foregroundColor(.secondary)
                    Spacer()
                    Text(asset.amount)
                        .font(.title2)
                        .foregroundColor(.primary)
                }
                Divider()
            }
        }
        .padding()
    }
}

struct AssetSectionView_Previews: PreviewProvider {
    static var previews: some View {
        let asset = Asset(
            id: 0,
            type: .bankAccount,
            data: [
                AssetData(id: 0, title: "신한은행", amount: "5,300,000원"),
                AssetData(id: 1, title: "국민은행", amount: "700,000원"),
                AssetData(id: 2, title: "카카오뱅크", amount: "112,900,000원")
            ]
        )
        AssetSectionView(assetSection: asset)
    }
}

 

🟠 PageViewController.swift

//
//  PageViewController.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI
import UIKit

struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        return pageViewController
    }

    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController
        var controllers = [UIViewController]()

        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let index = controllers.firstIndex(of: viewController) else { return nil }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let index = controllers.firstIndex(of: viewController) else { return nil }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
               let visibleViewController = pageViewController.viewControllers?.first,
               let index = controllers.firstIndex(of: visibleViewController) {
                parent.currentPage = index
            }
        }
    }
}

 

🟠 PageControl.swift

//
//  PageControl.swift
//  MyAssets
//
//  Created by Hamlit Jason on 2022/02/23.
//

import SwiftUI
import UIKit

struct PageControl: UIViewRepresentable {
    var numberOfPages: Int
    @Binding var currentPage: Int
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UIPageControl {
        let control = UIPageControl()
        control.numberOfPages = numberOfPages
        
        control.addTarget(
            context.coordinator,
            action: #selector(Coordinator.updateCurrentPage(sender:)),
            for: .valueChanged
        )
        
        return control
    }
    
    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPage
    }
    
    class Coordinator: NSObject {
        var control: PageControl
        
        init(_ control: PageControl) {
            self.control = control
        }
        
        @objc func updateCurrentPage(sender: UIPageControl) {
            control.currentPage = sender.currentPage
        }
    }
}