struct SearchBarView: View {
@State var searchText: String = ""
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(
searchText.isEmpty ? Color.theme.secondaryText : Color.theme.accent
)
TextField("Search by name or symbol...", text: $searchText)
.foregroundColor(Color.theme.accent)
.overlay(alignment: .trailing) {
Image(systemName: "xmark.circle.fill")
.padding()
.offset(x: 10)
.foregroundColor(Color.theme.accent)
.background(.red)
}
}
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.theme.background)
.shadow(color: Color.theme.accent.opacity(0.15), radius: 10, x: 0, y: 0)
)
.padding()
}
}
텍스트 필드의 오버레이로 xmark를 만들어주는데 지금처럼 약간의 padding()과 .offset을 준 이유는 터치되는 공간을 넓게 만들어주기 위해서임
아니면 xmark가 터치되는 범위가 엄청 작아서 좋은 UX가 될수 없으
서치바뷰의 경우 외부의 뷰에서 값을 연동해서 사용하게 되기 때문에
@State로 선언했던걸 @Binding으로 바꿔줬다
홈뷰로 돌아와서 SearchBarView를 넣어주고
홈뷰모델에 searchText를 넣어서 뷰모델의 searchText를 바인딩해주면 끝!
다시 말하자면 지금만든 SearchBarView를 Binding으로 선언해서 더 유연하고 재사용가능하게 사용하는 컴포넌트를 만들 수 있었다는 거!!
텍스트필드의 x버튼이 눌러지면 키보드가 내려가게 해보자
UIApplication의 extension을 만들어주고
텍스트필드에 방금 만든 메소드 삽입!
오케이~
뷰모델의 searchText값이 바뀔 때 뭔가 바뀌게 되니까
새로운 subscriber를 만들면 될 것 같다
이렇게 sink하려고 준비하다보니까 allCoins랑 같이 combine되면 베스트일 거 같지않음?
두가지의 밸류가 방출되는 걸 알게됐고!
sink하기 전에 map을 써서 바꿔주면 참 좋을 거 같음
text가 비어있다면 [CoinModel]만 리턴하게끔 가드문을 작성하고
들어온 text를 .lowercased()로 한번 걸러준다
(대문자 소문자 구분을 하게되기 때문에 .lowercased로 그냥 다 통일 시키는 느낌)
filter를 사용해서 startingCoin에 텍스트가 포함된 친구들을 걸러주고
이 array를 반환하는 로직!
바로 return 하는 걸로 위에꺼보다 코드 줄여주고
이전에 있었던 $allCoins 로직이 필요없게됐다!
지금 combineLatest로 데이터가 같이 방출되고 있고,
searchText의 값이 방출되지 않더라도 [CoinModel]이 나오게끔 만들어서 이전에 있던 subscribtion은 지워줘도 될 거 같음
map에 들어간 로직을 따로 빼서 더 보기 좋게 만들자
🤔되게 헷갈렸었는데 이제 이해가 된부분이 있다!
addSubscriber()가 dataService의 allCoins랑 콤바인이 되서 퍼블리셔가 되는 게 조금 헷갈렸음
dataService의 allCoins가 들어오는 거랑 /
HomeViewModel이 가진 allCoins는 각각 다른 Array임!!
그니까 dataService의 allCoins는 코인모델 싸그리다 다운받아온 거 계속 저장되어 있는거고
우리는 그 값을 text와 묶어줌으로써 map이라는 메소드를 사용해서 또다른 새로운 [CoinModel]을 방출해주고, sink를 통해서 HomeViewModel의 allCoins랑 묶어주는거!!
map이나 filter 같은 modifier는 처음의 Array에 접근해서 직접적으로 배열을 바꾸는 게 아니라 들어온 데이터(배열)를 이용해서 또다른 새로운 Array를 리턴해주는 메소드임!! 기억해두기💡
Market의 통계를 나타낼 뷰를 추가해보자
그러기 위해선 새로운 Model이 필요함
StatisticModel이라는 모델을 만들어주고 percentageChange 같은 경우
옵셔널으로 선언하게 됐음!
그래서 init될 때 꼭 값이 필요하진 않아서 초깃값을 init()구문안에서 nil로 넣어주게 됐다!!
Statistic뷰를 새로 만들어주고 이거 프리뷰에서 계속 쓰게될 모델이라
프리뷰프로바이더 extension에 이렇게 추가해줬다!
모델에 있는 내용을 뷰로 그려주려고 하는데
제일 아래에 있는 percentageChange같은 경우 옵셔널이잖음
보통은 if let 으로 처리해줬을 텐데 지금처럼 opacity로 설정한 건
프레임 크기를 유지해주기 위해서!
마켓 데이터를 다운받아올겨
근데 코인들 받았던 api랑은 같이 연동 시키면 안됨
코인게코
코인게코 문서에서 global을 excute!
MarketDataModel이라는 파일을 만들고
JSON Data랑 URL을 넣어줌! (체크를 위해서)
그리고 JSON Data를 app.quicktype.io에 붙여서 모델을 가지고온다!
모델 이름이랑 필요없는 프로퍼티는 지워주고
CodingKeys 구성해줌
totalMarketCap 데이터를 보면 엄청 많은 것들이 있는데
이 중에 usd 데이터를 받아올 거임
새로운 변수로 요렇게 만들어주면 되지만 더 줄여볼 수 있겠죠?
volume이랑 btcDominance라는 변수도 만들어주고!!
홈뷰모델에서 사용될 마켓 dataService 만들어주자
기존에 있던 CoinDataService그대로 복사해서 조금 수정해주면 될듯
import Foundation
import Combine
class MarketDataService {
@Published var marketData: MarketDataModel? = nil
var marketDataSubscription: AnyCancellable?
init() {
getData()
}
private func getData() {
guard let url = URL(string: "https://api.coingecko.com/api/v3/global") else { return }
marketDataSubscription = NetworkingManager.download(url: url)
.decode(type: GlobalData.self, decoder: JSONDecoder())
.sink(receiveCompletion: NetworkingManager.handleCompletion,
receiveValue: { [weak self] returnedGlobalData in
self?.marketData = returnedGlobalData.data
self?.marketDataSubscription?.cancel()
})
}
}
네트워킹 매니저를 따로 빼놓은 게 코드 수정하는 게 이렇게나 편하게 된답니다
이제 홈뷰모델에 만든 DataService를 추가해줍시다
addSubscriber()에 marketDataService의 marketData를 map으로 StatisticModel로 변형 시켜줍시다~!
그리고 sink에서 statistics에 넣어주면 끝!
map에 있는 로직 메소드 새로 파서 밖으로 따로 빼주면
Nice and Clean~~
근데 숫자가 좀 읽기 어렵게 나오니까 Double Extension추가해주자
func formattedWithAbbreviations() -> String {
let num = abs(Double(self))
let sign = (self < 0) ? "-" : ""
switch num {
case 1_000_000_000_000...:
let formatted = num / 1_000_000_000_000
let stringFormatted = formatted.asNumberString()
return "\(sign)\(stringFormatted)Tr"
case 1_000_000_000...:
let formatted = num / 1_000_000_000
let stringFormatted = formatted.asNumberString()
return "\(sign)\(stringFormatted)Bn"
case 1_000_000...:
let formatted = num / 1_000_000
let stringFormatted = formatted.asNumberString()
return "\(sign)\(stringFormatted)M"
case 1_000...:
let formatted = num / 1_000
let stringFormatted = formatted.asNumberString()
return "\(sign)\(stringFormatted)K"
case 0...:
return self.asNumberString()
default:
return "\(sign)\(self)"
}
}
숫자 크기에 따라서 보기 쉽게 반올림과 단위들을 붙여주는 익스텐션이다
+가 눌릴 때 포트폴리오 뷰를 띄워줄 예정
근데 지금 한가지 문제가 있음
새로운 뷰로 진입이 바뀌는 거라서 NavigationView가 안먹힌다는거!!
Portfolio View자체에 NavigationView를 삽입해줘야 된다
닉은 예전 방법으로 툴바 구현했지만 나는 요즘 표현으로 구현함
(호오오 나중에 설명해주고 바꿔주네용)
지금이 Button은 따로 컴포넌트로 빼주면 언제든 재사용 가능할 것 같다
컴포넌트 버튼을 들고 있는 PortfolioView에서도 dimiss 환경변수를 작성하고, 이걸 연결해줘야 디스미스 액션이 제대로 작동함!
포트폴리오 뷰에 서치바를 넣어주자
그리고 코인LogoView도 만들어줌
포트폴리오뷰에서 CoinLogoView를 넣어주면
쫜
디자인 정돈해주고
selectedCoin과 coin의 id가 같은지의 여부에 따라 색깔이 변하게 해줌
이제 선택이되면 아래로 상호작용할 수 있는 뷰들이 보여지게 만들어야하는데
animation을 none으로 설정하려면 새로운 animationValue를 선언해줘야한다!
.animation의 경우 optional값을 밸류로 넣어줄 수 없어서!!
포트폴리오 inputSection을 따로 빼주고
바디 깔끔하게 정리끝!
저장 버튼을 만들어주는데
로직은 요렇게 구성해줬다!
그리고 포트폴리오뷰의 navigationTitle 색상을 변경해주고 싶음
처음 init될 때 오버라이드 해주면 됩니다:)