Notice
Recent Posts
Recent Comments
Link
ยซ   2024/05   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
๊ด€๋ฆฌ ๋ฉ”๋‰ด

lgvv98

part4 (ch1). MyAssets ์ฝ”๋“œ๋ฆฌ๋ทฐ(feat. SwiftUI) ๋ณธ๋ฌธ

โš ๏ธ deprecated โš ๏ธ/ํŒจ์บ (์ดˆ๊ฒฉ์ฐจ)

part4 (ch1). MyAssets ์ฝ”๋“œ๋ฆฌ๋ทฐ(feat. SwiftUI)

๐Ÿฅ• ์บ๋Ÿฟ๋งจ 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
        }
    }
}
Comments