[SwiftUI Crypto App] Project Setup (15~19)

Woozoo·2023년 3월 1일
0

[SwiftUI Review]

목록 보기
10/41
post-custom-banner

Saving to Core Data

포트폴리오는 저장되어야 함
코어데이터를 사용해보자

그전에 지금 포트폴리오뷰의 textField에서 x버튼을 누르면 아래에 있는 list가 사라지지 않는 현상이 있음 이거 수정하고 진행합시다

어렵지않음
단순히 .onChange일 때 아까 작성했던 removeSelectedCoin() 호출해주면 끝


이제 코어데이터 작성해봅시다


Entity 만들어주고

class PortfolioDataService {
    
    private let container: NSPersistentContainer
    private let containerName: String = "PortfolioContainer"
    private let entityName: String = "PortfolioEntity"
    
    @Published var savedEntities: [PortfolioEntity] = []
    
    init() {
        container = NSPersistentContainer(name: containerName)
        container.loadPersistentStores { _, error in
            if let error = error {
                print("Error loading Core Data! \(error)")
            }
            self.getPortfolio()
        }
    }
    
    //MARK: - Public
    
    func updatePortfolio(coin: CoinModel, amount: Double) {
        // check if coin is already in portfolio
        if let entity = savedEntities.first(where: { $0.coinID == coin.id}) {
            if amount > 0 {
                update(entity: entity, amount: amount)
            } else {
                delete(entity: entity)
            }
        } else {
            add(coin: coin, amount: amount)
        }
    }
    
    
    //MARK: - Private
    private func getPortfolio() {
        let request = NSFetchRequest<PortfolioEntity>(entityName: entityName)
        do {
            savedEntities = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching Portfolio Entities. \(error)")
        }
    }
    
    private func update(entity: PortfolioEntity, amount: Double) {
        entity.amount = amount
        applyChanges()
    }
    
    private func delete(entity: PortfolioEntity) {
        container.viewContext.delete(entity)
        applyChanges()
    }
    
    private func add(coin: CoinModel, amount: Double) {
        let entity = PortfolioEntity(context: container.viewContext)
        entity.coinID = coin.id
        entity.amount = amount
        applyChanges()
    }
    
    private func save() {
        do {
            try container.viewContext.save()
        } catch let error {
            print("Error saving to Core Data. \(error)")
        }
    }
    
    private func applyChanges() {
        save()
        getPortfolio()
    }
    
}

add, delete, update 같은 메소드들을 구성
그리고 public으로 사용하게 될 updatePortfolio를 만들어줌
PortfolioDataService를 이제 홈뷰모델에서 만들어주자


홈뷰모델이 가진 allCoins의 값과 portfolioDataService에 있는 savedEntities의 값을 듣는 Subscriber를 만들어줌



Reloading Coin Data

포트폴리오 밸류의 변화를 StatisticModel에 넘겨주는 작업을 해보자


이렇게!!


HomeViewModel의 addSubscriber 쪽에서
marketDataservice 섭스크라이브 로직에 combineLatest로 $portfolioCoins도 같이 내려오게 해줌


portfolioCoins를 맵해서 currentHoldingsValue만 빼오고
그럼 [Double] 타입의 어레이가 나올텐데 .reduce라는 메소드를 사용하면 다 합쳐줄 수 있음!

그래서 portfolio밸류를 넘겨주고

이제 percentageChange를 만들어봅시다

약간 수학시간이긴 하지만 ㅋㅋㅋ
percentageChange를 계산해서 넘겨줬습니다


기존에 있던 allCoins 섭스크라이버 map 쪽에 function으로 정리가 안되어 있어서 이거 수정해줍시다


reload 만들어봅시다
아쉽게도 swiftUI에선 당겨서 업뎃하는 기능이 없어용

그냥 버튼으로 만들죠

우선 CoinDataService의 private을 지워줍시다
MarketDataService도 지워주고용


그리고 뷰모델로 돌아와서 이렇게 reloadData()를 만들어주는 데
이거 어떤 플로우인지 암?


데이터 서비스들의 데이터를 업뎃해주면
제일 위에부터 아래로 흘러가게됨!!! 샤라락


그리고 홈뷰 버튼에 추가해주면 되겠죠~!
rotationEffect까지!


아쉽지만 리로드될때 햅틱매니저라도 추가해줍시다

유틸리즈 폴더에 새로운 파일 작성해주고


reload에 추가해줍시다

🤔static private이 뭐지???


Adding a sorting feature

sort할 수 있는 기능을 구현해주자
프라이스 높은 순이나 market cab 높은 순처럼!


뷰모델에 enum을 만들어줬다
그리고 sortOption이라는 @Published 변수를 만들어줌


그리고 allCoins와함께 $sortOption도 같이 받아줌



전에 있던 filteredCoins를 호출하고 이걸 sort해주면 될 거 같다

선택된 sort에 따라서 분기처리
return 으로 coins를 sorted by 해주는 데 위에 꺼가 짧은 표현!

앱을 처음 실행하게 되면 holdings의 value는 없기 때문에 우선 .rank와 .rankReversed에 넣어줌


근데 지금 보면 sortedCoins 메소드는 [CoinModel]을 받아서 새로운[CoinModel]을 뱉고 있잖음
이거 더 효율적으로 바꿀 수 있음

inout을 coins에 붙여주고 return하던 걸 다 없애줌!!
그리고 sorted by 메소드도 sort로!!

🤔 inout 키워드가 뭘까?


그리고 sortCoins를 호출할 때 &을 붙여주면 들어온 걸 바로 수정해주게 됨


이제 searchTextallCoins,sortOption의값이바뀌면searchText랑 allCoins, sortOption의 값이 바뀌면 allCoins로 내려오는 아래의 portfolioCoins도 바뀌게 될텐데
이 때 필요한 경우에 holdings 밸류에 따라 정렬을 바꿔보자

이렇게 작성해주고나면 이제 sortOption만 바뀌게 되도 다 바뀐다!!
신기하지 않음?
이게 combine의 힘

홈뷰로 돌아와서

기존의 Text들 요렇게 바꿔줌!


디테일뷰를 만들고 홈뷰의 allCoinsList에 NavigationLink를 구성해줌!
근데 이렇게 구현하면

코인리스트가 init될 때 DetailView도 함께 init되게 된다..!!
만약에 엄청 많은 내용을 가진 뷰가 뒤에서 init되버린다면?
앱은 느려지겠죠
거기에 뷰모델까지 가지고 있고 api콜을 처리하고 있다면 더 부하가 생깁니당

그러니까 간단한 뷰가 아니라면 NavigationLink 보다는 다른 방법을 사용해야겠죠


segue라는 메소드를 만들고 onTapGesture에서 호출해줬습니다
UIKit 세그랑 비슷하게 만들라나봄


background로 NavigationLink를 붙여주는데 isActive 값에따라서 할 예정!
(아쉽게도 지금작성된 표현은 deprecated 되었는데)

🤔 요즘은 어떻게 구현할 수 있을까?

디테일뷰로 다시 돌아와서 let 으로 선언되어 있던 걸 Binding으로 바꿔줄거!
Binding이 있는 경우의 init은 지금처럼 만들어줘야합니다

destination에 selectedCoin 바인딩해주고


segue메소드에 selectedCoin을 넘겨주면

필요할때만 init됩니다~!

한 가지 더


뷰의 구조를 이렇게 DetailView를 DetailLoadingView에서 if let 으로 받아서 정말 값이 있을 때만 뜨게 해줄 수 있음

허허 근데
Init이 두번씩 뜬다?

🤔 댓글에도 많았던 질문 init 두번씩 떠여

다음은 ux 수정해주기
지금 리스트에서 중간에 빈 공백을 눌렀을 땐 터치가 안됨

CoinRowView의 백그라운드에 opacity 아주 사아알짝 줘서
터치가능하게 해줌 ㅋㅋㅋ..


Creating CoinDetailModel

디테일뷰에서 사용할 api호출이 필요한데 거기에 맞는 model도 필요할 거 같다


코인게코에서 {id}항목 사용할 거임

근데 이거 excute해보면 어어어엄청 많은 데이터를 가져오는 걸 볼 수 있음

쿼리 다시 작성해주고 실행!


CoinDetailModel 만들고 url이랑 response 입력


모델 필요한 데이터만 작성하고
Codable되게 해주고!

class CoinDetailDataService {
    @Published var allCoins: [CoinModel] = []
    var coinSubscription: AnyCancellable?
    
    init() {
        getCoinDetails()
    }
    
    func getCoinDetails() {
        guard let url = URL(string: "https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false")
        else { return }
        
        coinSubscription = NetworkingManager.download(url: url)
            .decode(type: [CoinModel].self, decoder: JSONDecoder())
            .sink(receiveCompletion: NetworkingManager.handleCompletion,
                  receiveValue: { [weak self] returnedCoins in
                self?.allCoins = returnedCoins
                self?.coinSubscription?.cancel()
            })
    }    
}

CoinDataService를 참고해서 CoinDetailDataService를 만들어줍시다

근데 여기서 api를 Call할 때 coin이름들을 개별적으로 넣어줄거잖음
그럼 코인을 넘겨줘야겠네?

class CoinDetailDataService {
    @Published var coinDetails: CoinDetailModel? = nil
    var coinDetailSubscription: AnyCancellable?
    let coin: CoinModel
    
    init(coin: CoinModel) {
        self.coin = coin
        getCoinDetails()
    }
    
    func getCoinDetails() {
        guard let url = URL(string: "https://api.coingecko.com/api/v3/coins/\(coin.id)?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false")
        else { return }
        
        coinDetailSubscription = NetworkingManager.download(url: url)
            .decode(type: CoinDetailModel.self, decoder: JSONDecoder())
            .sink(receiveCompletion: NetworkingManager.handleCompletion,
                  receiveValue: { [weak self] returnedCoinDetails in
                self?.coinDetails = returnedCoinDetails
                self?.coinDetailSubscription?.cancel()
            })
    }
}

init될 때 coin받을 수 있게해줌
그리고 published되는 것도 coinDetailModel이 되게해주고!!

디테일뷰로 와서

@StateObject를 init할 때 값을 넣어주게 만드는데

요부분 조금 논란이 될 수 있다. 애플은 @StateObject를 init에 넣지 말라고 권장함🤔 나중에 찾아보기!

실행해봅시다


오키 잘나옴


profile
우주형
post-custom-banner

0개의 댓글