1. 이전 포스팅 확인하기

 

https://growingsaja.tistory.com/944

 

 

 

 

 

2. 작업 전 앱 실행 결과 화면

 

 

 

 

 

 

3. 이번 목표

 

  a. 우측에 데이터 노출

      고가 Futures가 있는 A 거래소 이미지 / A거래소 Future의 괴리율 / A거래소의 Spot 가격 및 24h 변동률

      저가 Futures가 있는 B 거래소 이미지 / B거래소 Future의 괴리율 / B거래소의 Spot 가격 및 24h 변동률

 

  b. 괴리율 계산 방식

      (거래소 Futures 가격 - 거래소 Spot 가격) / 거래소 Spot 가격 x 100 (%)

 

 

 

 

 

4. View 구현 : PopularAssetListVC

 

 일단 데이터가 들어갈 기본 틀만 만들어줍니다.

 

// vim Controller/PopluarAssetListVC.h



// ...



// Futures
@property (strong, nonatomic) NSMutableArray *cryptoPriceFuturesFirstLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePercent24FuturesFirstLabelList; // Label
@property (strong, nonatomic) NSMutableArray *futuresFirstExchangeImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceFuturesSecondLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePercent24FuturesSecondLabelList; // Label
@property (strong, nonatomic) NSMutableArray *futuresSecondExchangeImageList; // ImageView

@property (strong, nonatomic) NSMutableArray *disparityRatioFirstLabelList; // Label
@property (strong, nonatomic) NSMutableArray *disparityRatioFirstExchangeImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *disparityRatioSecondLabelList; // Label
@property (strong, nonatomic) NSMutableArray *disparityRatioSecondExchangeImageList; // ImageView

/* #################### 데이터 세팅 #################### */
// default.json의 exchangeInfo 저장
@property (strong, nonatomic) NSDictionary *exchangeInfo;

// 카드 안의 정보들을 최신 데이터로 뷰 다시 그려주기
-(void) updateCardView;

// 해당 화면 전체 뷰 최신화하기
-(void) updateView;

-(void) loadPopularList;


@end

 

// vim PopularAssetListVC.m






// ...





        /* #################### 거래소별로 Spot & Futures 괴리율 도출해 큰 거래소쪽 정보 노출 세팅 #################### */
        // ****** High spot 현재 가격, 거래소 정보 노출 라벨 세팅 ****** //
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceFuturesFirstLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 4, basicMarginInCard, cardViewWidth / 5, 20)];
        cryptoPriceFuturesFirstLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceFuturesFirstLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePercent24FuturesFirstLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 17, basicMarginInCard, cardViewWidth/3, 20)];
        changePercent24FuturesFirstLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *futuresFirstExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 10 * 9, basicMarginInCard, miniimage, miniimage)];
        futuresFirstExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        // ****** Low spot 현재 가격, 거래소 정보 노출 라벨 세팅 ****** //
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceFuturesSecondLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 4, cardViewHeight/2, cardViewWidth / 5, 20)];
        cryptoPriceFuturesSecondLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceFuturesSecondLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePercent24FuturesSecondLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 17, cardViewHeight/2, cardViewWidth/3, 20)];
        changePercent24FuturesSecondLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *futuresSecondExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 10 * 9, cardViewHeight/2, miniimage, miniimage)];
        futuresSecondExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        // first rank disparity ratio
        [cardView addSubview:cryptoPriceFuturesFirstLabel];
        [_cryptoPriceFuturesFirstLabelList addObject:cryptoPriceFuturesFirstLabel];
        [cardView addSubview:changePercent24FuturesFirstLabel];
        [_changePercent24FuturesFirstLabelList addObject:changePercent24FuturesFirstLabel];
        [cardView addSubview:futuresFirstExchangeImage];
        [_futuresFirstExchangeImageList addObject:futuresFirstExchangeImage];
        
        // second rank disparity ratio
        [cardView addSubview:cryptoPriceFuturesSecondLabel];
        [_cryptoPriceFuturesSecondLabelList addObject:cryptoPriceFuturesSecondLabel];
        [cardView addSubview:changePercent24FuturesSecondLabel];
        [_changePercent24FuturesSecondLabelList addObject:changePercent24FuturesSecondLabel];
        [cardView addSubview:futuresSecondExchangeImage];
        [_futuresSecondExchangeImageList addObject:futuresSecondExchangeImage];






// ...

 

 

 

 

 

5. View 구현 : PopularAssetListVC 소스코드 수정

 

// vim Controller/PopluarAssetListVC.h

#import <UIKit/UIKit.h>

// SecondViewController라는 이름의 뷰 컨트롤러 클래스를 선언합니다.
@interface PopluarAssetListVC : UIViewController
@property (readwrite, strong, nonatomic) UIScrollView *scrollView;
@property (readwrite, strong, nonatomic) UIStackView *stackView;


/* #################### 뷰 기본 세팅 #################### */

@property (strong, nonatomic) UILabel *UILabel;

// 탑 뷰
@property (strong, nonatomic) UIView *topInfoTab;
// 라벨 : 현재 시간
@property (strong, nonatomic) UILabel *earthLabel;
@property (strong, nonatomic) UILabel *liveUTCLabel;
@property (strong, nonatomic) UILabel *koreanFlagLabel;
@property (strong, nonatomic) UILabel *liveKSTLabel;
// 라벨 : 환율
@property (strong, nonatomic) UIImageView *ratesProviderImage;
@property (strong, nonatomic) UILabel *ratesLabel;
@property (strong, nonatomic) UILabel *ratesUpdateDateTimeLabel;

// 라벨 : 카드
@property (strong, nonatomic) NSMutableArray *cardViewList; // Label
@property (strong, nonatomic) NSMutableArray *cryptoNameLabelList; // Label

// Spot
@property (strong, nonatomic) NSMutableArray *cryptoPriceHighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePricePercent24HighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoHighImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceLowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePricePercent24LowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoLowImageList; // ImageView

@property (strong, nonatomic) NSMutableArray *maxPricePremiumPercentLabelList; // Label
@property (strong, nonatomic) NSMutableArray *maxPremiumTabHighExchangeImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *maxPremiumTabLowExchangeImageList; // ImageView

// Futures
@property (strong, nonatomic) NSMutableArray *cryptoPriceFuturesFirstLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePercent24FuturesFirstLabelList; // Label
@property (strong, nonatomic) NSMutableArray *futuresFirstExchangeImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceFuturesLastLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePercent24FuturesLastLabelList; // Label
@property (strong, nonatomic) NSMutableArray *futuresLastExchangeImageList; // ImageView

@property (strong, nonatomic) NSMutableArray *disparityRatioFirstRankLabelList; // Label
@property (strong, nonatomic) NSMutableArray *disparityRatioLastRankLabelList; // Label

// 카드 안의 정보들을 최신 데이터로 뷰 다시 그려주기
-(void) updateCardView;

// 해당 화면 전체 뷰 최신화하기
-(void) updateView;

-(void) loadPopularList;


@end

 

// vim PopluarAssetListVC.m

#import <Foundation/Foundation.h>
#import "PopluarAssetListVC.h"
// default.json 데이터 읽기
#import "DefaultLoader.h"
// 수시로 변경할 수 있는 설정값
#import "CustomizeSetting.h"
// 현재 시간 가져오는 용도
#import "DateTime.h"
// price 정보 저장
#import "AllPriceLiveData.h"
// 환율 서비스
#import "ServiceRecentRates.h"

@implementation PopluarAssetListVC {
    // json에서 가져온 popularList raw 데이터
    NSArray *popularList;
    NSArray *exchangeListSpot;
    NSArray *exchangeListFutures;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // viewDidLoad 메서드에서 스크롤 뷰를 초기화하고 설정합니다.
    // 스크롤 뷰 생성 및 초기화
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.backgroundColor = [UIColor clearColor];
    [self.view addSubview:self.scrollView];
    
    // 스택 뷰 생성 및 초기화
    self.stackView = [[UIStackView alloc] initWithFrame:self.scrollView.bounds];
    self.stackView.axis = UILayoutConstraintAxisVertical;
    self.stackView.distribution = UIStackViewDistributionEqualSpacing;
    self.stackView.alignment = UIStackViewAlignmentFill;
    self.stackView.spacing = 0;
    [self.scrollView addSubview:self.stackView];
    
    // default.json의 필요 데이터 가져오기
    [self loadExchangeInfo];
    [self loadPopularList];
    
    // 데이터를 표시할 레이블 생성
    // **************************************** [Start] 뷰 그리기 **************************************** //
    CGFloat defaultFontSize = 16.0;
    //카드뷰 배치에 필요한 변수를 설정합니다.
    // 카드 목록 나열될 공간 세팅
    // ****************************** //
    // 카드 자체에 대한 세팅
    // 카드 높이 길이 (상하 길이) 설정
    CGFloat cardViewHeight = defaultFontSize * 2;
    // 카드 좌우 길이 phone size 참조하여 자동 조정
    CGFloat cardViewWidth = [UIScreen mainScreen].bounds.size.width;
    // ****************************** //
    
    // **************************************** [Start] 최상단에 기타 정보 공간 **************************************** //
    /* #################### 현재 시간 정보 #################### */
    self.topInfoTab = [[UIView alloc] initWithFrame:CGRectMake(0,-cardViewHeight, cardViewWidth, cardViewHeight)];
    // 국기 너비 길이 (좌우 길이) 설정
    CGFloat miniimage = 14.0;
    
    // ****** 글로벌 지구 이모티콘 및 시간 설정 ****** //
    self.earthLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, miniimage * 3 / 2, miniimage)];
    self.earthLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:miniimage];
    self.earthLabel.text = @"🌍";
    // 표준 시간 = 그리니치 표준시 : 더블린, 에든버러, 리스본, 런던, 카사블랑카, 몬로비아
    self.liveUTCLabel = [[UILabel alloc] initWithFrame:CGRectMake(miniimage * 3 / 2, 0, cardViewWidth / 2, miniimage)];
    self.liveUTCLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    // ****** 한국 이모티콘 및 시간 설정 ****** //
    // 태극기
    self.koreanFlagLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, defaultFontSize, miniimage * 3 / 2, miniimage)];
    self.koreanFlagLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:miniimage];
    self.koreanFlagLabel.text = @"🇰🇷";
    // 한국 시간
    self.liveKSTLabel = [[UILabel alloc] initWithFrame:CGRectMake(miniimage * 3 / 2, defaultFontSize, cardViewWidth / 2, miniimage)];
    self.liveKSTLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    /* #################### 환율 정보 #################### */
    // usdkrw 정보 출처 아이콘 이미지를 위한 기본 셋
    self.ratesProviderImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth/12*7, 0, miniimage, miniimage)];
    self.ratesProviderImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
    // usdkrw 환율
    self.ratesLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth/12*7, 0, cardViewWidth/8*3, defaultFontSize)];
    self.ratesLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesLabel.textAlignment = NSTextAlignmentRight;
    // 기준 시간
    self.ratesUpdateDateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth/12*7, defaultFontSize, cardViewWidth/8*3, defaultFontSize)];
    self.ratesUpdateDateTimeLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesUpdateDateTimeLabel.textAlignment = NSTextAlignmentRight;
    
    // ****** 상단에 배치할 Label들을 topInfoTab View에 추가 ****** //
    // global 적용
    [self.topInfoTab addSubview:_earthLabel];
    [self.topInfoTab addSubview:_liveUTCLabel];
    // korea 적용
    [self.topInfoTab addSubview:_koreanFlagLabel];
    [self.topInfoTab addSubview:_liveKSTLabel];
    // 환율 적용
    [self.topInfoTab addSubview:_ratesProviderImage];
    [self.topInfoTab addSubview:_ratesLabel];
    [self.topInfoTab addSubview:_ratesUpdateDateTimeLabel];
    
    // ****** 상단 UIView를 self.scrollView에 추가 ****** //
    [self.scrollView addSubview:_topInfoTab];
    
    
    // **************************************** [Start] 카드뷰 목록 쭉 만들기 **************************************** //
    // 카드뷰 큰 틀
    self.cardViewList = [@[] mutableCopy];
    // 암호화폐 symbol
    self.cryptoNameLabelList = [@[] mutableCopy];
    // 암호화폐 Spot 주요 정보
    self.cryptoPriceHighLabelList = [@[] mutableCopy];
    self.changePricePercent24HighLabelList = [@[] mutableCopy];
    self.exchangeLogoHighImageList = [@[] mutableCopy];
    self.cryptoPriceLowLabelList = [@[] mutableCopy];
    self.changePricePercent24LowLabelList = [@[] mutableCopy];
    self.exchangeLogoLowImageList = [@[] mutableCopy];
    // 암호화폐 Spot 프리미엄
    self.maxPricePremiumPercentLabelList = [@[] mutableCopy];
    self.maxPremiumTabHighExchangeImageList = [@[] mutableCopy];
    self.maxPremiumTabLowExchangeImageList = [@[] mutableCopy];
    // 암호화폐 Spot & Futures Disparity 관련 주요 정보
    self.cryptoPriceFuturesFirstLabelList = [@[] mutableCopy];
    self.changePercent24FuturesFirstLabelList = [@[] mutableCopy];
    self.futuresFirstExchangeImageList = [@[] mutableCopy];
    self.cryptoPriceFuturesLastLabelList = [@[] mutableCopy];
    self.changePercent24FuturesLastLabelList = [@[] mutableCopy];
    self.futuresLastExchangeImageList = [@[] mutableCopy];
    // 암호화폐 Spot & Futures Disparity 관련 프리미엄 정보
    self.disparityRatioFirstRankLabelList = [@[] mutableCopy];
    self.disparityRatioLastRankLabelList = [@[] mutableCopy];
    
    
    
    for (int i=0; i<popularList.count; i++) {
        /* #################### 카드뷰 기본 세팅 #################### */
        UIView *cardView = [[UIView alloc] initWithFrame:CGRectMake(0, i * cardViewHeight, cardViewWidth, cardViewHeight)];
        
        // UILabel의 텍스트 색상 및 배경색 설정
        // 카드뷰 배경색을 설정합니다.
        if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            // 다크모드인 경우
            cardView.backgroundColor = [UIColor blackColor];
            [self.view addSubview:cardView];
            // 카드 테두리 다크그레이색
            cardView.layer.borderColor = [UIColor darkGrayColor].CGColor;
        } else {
            // 라이트모드인 경우
            cardView.backgroundColor = [UIColor whiteColor];
            [self.view addSubview:cardView];
            // 카드 테두리 다크그레이색
            cardView.layer.borderColor = [UIColor lightGrayColor].CGColor;
        }
        
        // 카드 테두리 두께
        cardView.layer.borderWidth = 0.5;
        
        // 카드뷰 모서리를 둥글게 설정합니다. 조건 1
        cardView.layer.cornerRadius = 0.0;
        // cardView의 경계를 기준으로 내용물이 보이는 영역을 제한합니다. masksToBounds를 YES로 설정하면, cardView의 경계 밖에 있는 모든 내용물은 자르고 숨깁니다(클립 됩니다). 즉 뷰의 경계 값을 초과한 부분을 자르기 위해 masksToBounds를 YES로 설정합니다. 반면 masksToBounds가 NO인 경우(기본값)에는 뷰의 경계 밖에 있는 내용물이 그대로 보이게 됩니다.
        cardView.layer.masksToBounds = YES;
        
        // UILabel 객체를 생성합니다. 이 레이블은 암호화폐의 이름을 표시할 것입니다.
        // 따라서 CGRect를 사용하여 레이블의 위치와 크기를 설정하며, 왼쪽 위 모서리에서 시작합니다.
        UILabel *cryptoNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, cardViewWidth / 4, cardViewHeight/2)];
        
        cryptoNameLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
//        systemFontOfSize:defaultFontSize
        // 생성한 cryptoNameLabel을 cardView의 서브뷰로 추가합니다. 이렇게 함으로써 레이블이 카드 뷰에 표시됩니다.
        [_cryptoNameLabelList addObject:cryptoNameLabel];
        [cardView addSubview:cryptoNameLabel];
        
        /* #################### High spot 현재 가격, 거래소 정보 노출 라벨 세팅 #################### */
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceHighLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 3, 0, cardViewWidth/4, cardViewHeight/2)];
        cryptoPriceHighLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceHighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePricePercent24HighLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 16 * 7, 0, cardViewWidth/6, cardViewHeight/2)];
        changePricePercent24HighLabel.textAlignment = NSTextAlignmentRight;
        changePricePercent24HighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoHighImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 2, 0, miniimage, miniimage)];
        exchangeLogoHighImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### Low spot 현재 가격, 거래소 정보 노출 라벨 세팅 #################### */
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceLowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 3, cardViewHeight/2, cardViewWidth/4, cardViewHeight/2)];
        cryptoPriceLowLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceLowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePricePercent24LowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 16 * 7, cardViewHeight/2, cardViewWidth/6, cardViewHeight/2)];
        changePricePercent24LowLabel.textAlignment = NSTextAlignmentRight;
        changePricePercent24LowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoLowImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 2, cardViewHeight/2, miniimage, miniimage)];
        exchangeLogoLowImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### spot 프리미엄 노출 세팅 #################### */
        // 기본 셋
        UILabel *maxPricePremiumPercentLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, cardViewHeight/2, cardViewWidth / 8 + miniimage, cardViewHeight/2)];
        maxPricePremiumPercentLabel.textAlignment = NSTextAlignmentRight;
        maxPricePremiumPercentLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        // top rank 거래소 아이콘을 위한 기본 셋
        UIImageView *maxPremiumTabHighExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, cardViewHeight/2, miniimage, miniimage)];
        maxPremiumTabHighExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        // bottom rank 거래소 아이콘을 위한 기본 셋
        UIImageView *maxPremiumTabLowExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(miniimage + cardViewWidth / 8, cardViewHeight/2, miniimage, miniimage)];
        maxPremiumTabLowExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        // top rank spot
        [cardView addSubview:cryptoPriceHighLabel];
        [_cryptoPriceHighLabelList addObject:cryptoPriceHighLabel];
        [cardView addSubview:changePricePercent24HighLabel];
        [_changePricePercent24HighLabelList addObject:changePricePercent24HighLabel];
        [cardView addSubview:exchangeLogoHighImage];
        [_exchangeLogoHighImageList addObject:exchangeLogoHighImage];
        
        // bottom rank spot
        [cardView addSubview:cryptoPriceLowLabel];
        [_cryptoPriceLowLabelList addObject:cryptoPriceLowLabel];
        [cardView addSubview:changePricePercent24LowLabel];
        [_changePricePercent24LowLabelList addObject:changePricePercent24LowLabel];
        [cardView addSubview:exchangeLogoLowImage];
        [_exchangeLogoLowImageList addObject:exchangeLogoLowImage];
        
        // spot max premium % in spot
        [cardView addSubview:maxPricePremiumPercentLabel];
        [_maxPricePremiumPercentLabelList addObject:maxPricePremiumPercentLabel];
        [cardView addSubview:maxPremiumTabHighExchangeImage];
        [_maxPremiumTabHighExchangeImageList addObject:maxPremiumTabHighExchangeImage];
        [cardView addSubview:maxPremiumTabLowExchangeImage];
        [_maxPremiumTabLowExchangeImageList addObject:maxPremiumTabLowExchangeImage];
        
        /* #################### 거래소별로 Spot & Futures 괴리율 도출해 큰 거래소쪽 정보 노출 세팅 #################### */
        // ****** First Rank 현재 가격, 거래소 정보 노출 라벨 세팅 ****** //
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceFuturesFirstLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 4, 0, cardViewWidth / 5, defaultFontSize/5*3)];
        cryptoPriceFuturesFirstLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceFuturesFirstLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize/5*3];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePercent24FuturesFirstLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 40 * 37, cardViewHeight/4, cardViewWidth/8, defaultFontSize/7*4)];
        changePercent24FuturesFirstLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize/7*4];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *futuresFirstExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 40 * 29, 0, miniimage, miniimage)];
        futuresFirstExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        // first rank disparity ratio
        [cardView addSubview:cryptoPriceFuturesFirstLabel];
        [_cryptoPriceFuturesFirstLabelList addObject:cryptoPriceFuturesFirstLabel];
        [cardView addSubview:changePercent24FuturesFirstLabel];
        [_changePercent24FuturesFirstLabelList addObject:changePercent24FuturesFirstLabel];
        [cardView addSubview:futuresFirstExchangeImage];
        [_futuresFirstExchangeImageList addObject:futuresFirstExchangeImage];
        
        // ****** last rank 현재 가격, 거래소 정보 노출 라벨 세팅 ****** //
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceFuturesLastLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 4, cardViewHeight/2, cardViewWidth / 5, defaultFontSize/5*3)];
        cryptoPriceFuturesLastLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceFuturesLastLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize/5*3];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePercent24FuturesLastLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 40 * 37, cardViewHeight/4*3, cardViewWidth/8, defaultFontSize/7*4)];
        changePercent24FuturesLastLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize/7*4];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *futuresLastExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 40 * 29, cardViewHeight/2, miniimage, miniimage)];
        futuresLastExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        // last rank disparity ratio
        [cardView addSubview:cryptoPriceFuturesLastLabel];
        [_cryptoPriceFuturesLastLabelList addObject:cryptoPriceFuturesLastLabel];
        [cardView addSubview:changePercent24FuturesLastLabel];
        [_changePercent24FuturesLastLabelList addObject:changePercent24FuturesLastLabel];
        [cardView addSubview:futuresLastExchangeImage];
        [_futuresLastExchangeImageList addObject:futuresLastExchangeImage];
        
        // ****** Disparity Ratio Premium 정보, 해당하는 거래소 정보 노출 라벨 세팅 ****** //
        // 프리미엄 수치 기본 셋 - first rank
        UILabel *disparityRatioFirstRankLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 4 * 3, 0, cardViewWidth/13*2, cardViewHeight/2)];
        disparityRatioFirstRankLabel.textAlignment = NSTextAlignmentRight;
        disparityRatioFirstRankLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 프리미엄 수치 기본 셋 - last rank
        UILabel *disparityRatioLastRankLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 4 * 3, cardViewHeight/2, cardViewWidth/13*2, cardViewHeight/2)];
        disparityRatioLastRankLabel.textAlignment = NSTextAlignmentRight;
        disparityRatioLastRankLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // disparity ratio Premium
        [cardView addSubview:disparityRatioFirstRankLabel];
        [_disparityRatioFirstRankLabelList addObject:disparityRatioFirstRankLabel];
        [cardView addSubview:disparityRatioLastRankLabel];
        [_disparityRatioLastRankLabelList addObject:disparityRatioLastRankLabel];
        
        /* #################### 카드뷰 세로로 축적하기 #################### */
        
        // cardView를 self.scrollView에 추가합니다.
        [self.scrollView addSubview:cardView];
        // 레이블 세팅 완료된 cardView를 CardViewList에 넣기
        [self.cardViewList addObject: cardView];
    }
    
    // 상하 스크롤 최대치 자동 설정
    CGFloat contentHeight = popularList.count * cardViewHeight;
    self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, contentHeight);
    

    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
    [NSTimer scheduledTimerWithTimeInterval:VIEW_UPDATE_TERM
                                     target:self
                                   selector:@selector(updateCardView)
                                   userInfo:nil
                                    repeats:YES];
}

// **************************************** 카드뷰 다시 그리기 **************************************** //
-(void) updateCardView {
    // 메인 스레드에서만 UI 업데이트 수행
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateView];
    });
}

// **************************************** 기본 데이터 세팅하기 **************************************** //
/* #################### default.json 파일의 exchangeInfo 데이터 읽기 #################### */
-(void) loadExchangeInfo {
    // ****** default.json의 exchangeInfo 불러오기 ****** //
    exchangeListSpot = [DefaultLoader sharedInstance].exchangeListSpot;
    exchangeListFutures = [DefaultLoader sharedInstance].exchangeListFutures;
}

/* #################### default.json 파일의 popularList 데이터 읽기 #################### */
-(void) loadPopularList {
    // ****** default.json의 popularList 불러오고 정상인지 1차 확인하기 ****** //
    // popularList에 있는 각 element들의 개수가 같은지 확인
    // popularList에 있는 데이터들 중, isAvailable이 true인 애들만 추출하기
    NSArray *popularList_raw = [[DefaultLoader sharedInstance].popularList mutableCopy];
    NSMutableArray *popularList_tmp = [@[] mutableCopy];
    int checkSizeOfEachPopularList = 0;
    for (int i=0; i<popularList_raw.count-1; i++) {
        // 각 popularAsset의 array 길이가 같은지 각각 확인해서 counting
        if ([popularList_raw[i] count] == [popularList_raw[i+1] count]) {
            checkSizeOfEachPopularList += 1;
        }
    }
    if (checkSizeOfEachPopularList == [popularList_raw count]-1) {
        // default.json파일의 popularList 안에 있는 Array들 중 길이가 모두 같으면 정상으로 판단하고 isActive 활성화 데이터만 사용
        for (NSArray *each in popularList_raw) {
            if ([each[0] boolValue]) {
                // index 0은 isAvailable 데이터로, 활성화 여부가 true인 애들만 넣어주기
                [popularList_tmp addObject:each];
            }
        }
        popularList = popularList_tmp;
        NSLog(@"%@", @"[INFO] Default Setting Load Complete");
    } else {
        // default.json파일의 popularList 안에 있는 Array들 중 길이가 다른 것이 1개라도 있으면 WARN 출력 및 available된 popularList 데이터 활용 미진행
        NSLog(@"****************************************************");
        NSLog(@"[WARN] Check File : default.json - popularList");
        NSLog(@"****************************************************");
    }
}

// **************************************** 전체 화면 뷰 **************************************** //
/* #################### 화면 업데이트 실시 #################### */
-(void) updateView {
    // 현재 시간 확인을 위한 singlton instance 생성
    DateTime *now = [DateTime sharedInstance];
    // 레이블의 텍스트를 설정합니다. 여기에서는 UTC 시간을 업데이트합니다.
    [now NowUTC: @"yyyy-MM-dd (E) HH:mm:ss"];
    self.liveUTCLabel.text = now.dateTime;
    // 레이블의 텍스트를 설정합니다. 여기에서는 KST 시간을 업데이트합니다.
    [now NowKST: @"yyyy-MM-dd (E) HH:mm:ss"];
    self.liveKSTLabel.text = now.dateTime;
    
    // ****** USDKRW 환율 정보 업데이트 ****** //
    ServiceRecentRates *ratesInfo = [ServiceRecentRates sharedInstance];
    // 환율 노출
    if (ratesInfo.usdkrw) {
        // 환율 변동률 가공 및 값에 따라 환율 정보 색 지정
        NSString *ratesChangeInfo = [ratesInfo.changePercentUsdkrw stringByAppendingString:@"%)"];
        if ( [ratesInfo.changePercentUsdkrw hasPrefix:@"-"]) {
            // 음수인 경우 이미 - 붙어있으니까 그냥 합치기
            ratesChangeInfo = [@" (" stringByAppendingString:ratesChangeInfo];
            // 음수인 경우 하락색
            self.ratesLabel.textColor = [UIColor redColor];
        } else {
            // 양수이거나 0인 경우  + 붙여서 합치기
            ratesChangeInfo = [@" (+" stringByAppendingString:ratesChangeInfo];
            if ([ratesInfo.changePercentUsdkrw isEqual:@"0.00"]) {
                // 0인 경우
                if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                    // 다크모드인 경우 글자색 흰색으로 원복
                    self.ratesLabel.textColor = [UIColor whiteColor];
                } else {
                    // 라이트모드인 경우 글자색 흑색으로 원복
                    self.ratesLabel.textColor = [UIColor blackColor];
                }
            } else {
                // 양수인 경우 상승색
                self.ratesLabel.textColor = [UIColor greenColor];
            }
        }
        // 환율 호가
        NSString *ratesMainInfo = ratesInfo.usdkrw;
        self.ratesLabel.text = [ratesMainInfo stringByAppendingString:ratesChangeInfo];
        // ****** rates 정보 출처 provider 이미지 설정 ****** //
        if ([ratesInfo.provider isEqual:@"하나은행"]) {
            self.ratesProviderImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].ratesApi[@"DunamuQuotation"][@"image"]];
        } else {
            self.ratesProviderImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].ratesApi[ratesInfo.provider][@"image"]];
            if ( ! [ratesInfo.provider isEqual:@"OpenExchangeRates"]) {
                NSLog(@"[INFO] Rates Image Loading Now...");
            }
        }
    } else {
        // 데이터 불러오기 실패 또는 불러오기 전인 경우
        if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            // 다크모드인 경우 글자색 흰색으로 원복
            self.ratesLabel.textColor = [UIColor whiteColor];
        } else {
            // 라이트모드인 경우 글자색 흑색으로 원복
            self.ratesLabel.textColor = [UIColor blackColor];
        }
        self.ratesLabel.text = @"-";
    }
    // 환율 최근 업데이트 일시 노출
    self.ratesUpdateDateTimeLabel.text = ratesInfo.recentDateTime;
    
    
    // ****** Spot 가격 정보 불러오기 ****** //
    AllPriceLiveData *allTicker = [AllPriceLiveData sharedInstance];
    
    /* #################### [Start] 카드뷰 데이터 업데이트 #################### */
    for (int i=0; i<popularList.count; i++) {
        
        // ****** 기준 key인 symbol 변수를 popularAsset 정보 토대로 만들기 ****** //
        NSString *asset = popularList[i][2];
        NSString *paymentCurrency = popularList[i][3];
        NSString *symbol = [asset stringByAppendingString:paymentCurrency];
        
        // ****** 카드뷰 기본 세팅 ****** //
        UILabel *cryptoNameLabel = _cryptoNameLabelList[i];
        cryptoNameLabel.text = symbol;
        
        // ****** 누가 High로 배치될지, Low로 배치될지 로직 ****** //
        // 거래소 가격 비교해보고 탑랭크, 바텀랭크 지정
        NSString *topRankPriceExchange = @"";
        NSString *bottomRankPriceExchange = @"";
        NSUInteger minIndex = 0;
        NSUInteger startIndex = 1;
        NSUInteger maxIndex = 0;
        // 해당 symbol이 상장되어있는 거래소 개수 counting하기
        NSUInteger aliveSymbolCount = 0;
        for (int i = 0; i<exchangeListSpot.count; i++) {
            if ([allTicker.recentSpotTicker[exchangeListSpot[i]] objectForKey:asset]) {
                // 특정 자산 있는지 확인
                if ([allTicker.recentSpotTicker[exchangeListSpot[i]][asset] objectForKey:paymentCurrency]) {
                    // 특정 지불 화폐 있는지 확인
                    aliveSymbolCount++;
                }
            }
        }
        if (aliveSymbolCount >= 1) {
            // 가격 정보가 1개 이상인 경우
            for (NSUInteger i=0; i<exchangeListSpot.count; i++) {
                // minIndex 배정 전에, 정상적인 유효 거래소 price의 index를 일단 찾아서 minIndex로 넣고 그거랑 비교 진행
                if (minIndex != startIndex && allTicker.recentSpotTicker[exchangeListSpot[i]][asset][paymentCurrency][@"price"]) {
                    // minIndex랑 startIndex가 다른, 초기 상태이면서 price가 null이 아닌 유효한 값을 가질 때에만 진행, 만약 if문 안에 들어온다면 minIndex와 startIndex, maxIndex가 같아지면서 해당 로직 미진행
                    // 목표는, 처음으로 값이 존재하는 exchange를 찾아 해당 거래소부터 대소비교를 진행하기 위함임!
                    startIndex = i;
                    minIndex = i;
                    maxIndex = i;
                }
            }
            for (NSUInteger i=0; i<exchangeListSpot.count; i++) {
                // min, max index 찾기 실행
                // 그 전에, Thread 1: EXC_BAD_ACCESS (code=1, address=...) 오류를 예방하기위해 자주 쓰는 변수 넣는 value 선언하기
                NSDictionary *priceOfAssetData = allTicker.recentSpotTicker[exchangeListSpot[i]];
                NSDictionary *priceOfAssetData_min = allTicker.recentSpotTicker[exchangeListSpot[minIndex]];
                NSDictionary *priceOfAssetData_max = allTicker.recentSpotTicker[exchangeListSpot[maxIndex]];
                if (minIndex == i) {
                    // min, max index 찾을때, startIndex는 어차피 minIndex에서 가져가면서 체크하기때문에 체크 미진행
                    // pass
                } else {
                    // min, max index 찾는 거 실행
                    if (priceOfAssetData[asset][paymentCurrency]) {
                        // index가 깊어서, 해당 깊이의 index가 존재하는지 확인한 후 대소 비교 진행하도록 if 조건문 실행 -> 만약 없으면 그냥 pass!
                        if (priceOfAssetData_min[asset][paymentCurrency]) {
                            if (priceOfAssetData_max[asset][paymentCurrency]) {
                                // 값이 없는 거래소의 경우 대소비교 미진행을 위한 예외처리
                                // !!! 한번에 여러 index에 접근하여 데이터를 참조하는 경우 잘못된 메모리 주소 참조를 방지할 수 있습니다. !!!
                                // 아예 수집을 하지 못한 거래소가 최소값인 거래소로 나오지 않도록, 없는 값은 아닌지 확인! null을 floatValue 하면 0.000000 이 나오기 때문에 무조건 작은 index로 잡힙니다. null인 애는 제외하고 대소비교를 해야합니다.
                                if ([priceOfAssetData[asset][paymentCurrency][@"price"] floatValue] < [priceOfAssetData_min[asset][paymentCurrency][@"price"] floatValue]) {
                                    // 가장 저렴한 Exchange index 찾기
                                    minIndex = i;
                                }
                                if ([priceOfAssetData[asset][paymentCurrency][@"price"] floatValue] > [priceOfAssetData_max[asset][paymentCurrency][@"price"] floatValue]) {
                                    // 가장 비싼 Exchange index 찾기
                                    maxIndex = i;
                                }
                            }
                        }
                    }
                }
            }
            // 찾은 최소 index와 최대 index를 통해 top, bottom 거래소명 값 저장
            topRankPriceExchange = exchangeListSpot[maxIndex];
            bottomRankPriceExchange = exchangeListSpot[minIndex];
        } else {
            // 가격 정보가 모두 없을 경우 (로딩중이거나 못불러오거나) 전부 0으로 노출
            topRankPriceExchange = exchangeListSpot[0];
            bottomRankPriceExchange = exchangeListSpot[0];
            allTicker.recentSpotTicker[topRankPriceExchange][asset][paymentCurrency][@"price"] = 0;
            allTicker.recentSpotTicker[bottomRankPriceExchange][asset][paymentCurrency][@"price"] = 0;
        }
        
        // ****** top price spot 거래소명 및 이미지 설정 ****** //
        UIImageView *exchangeLogoHighImage = _exchangeLogoHighImageList[i];
        exchangeLogoHighImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"information"][@"image"]];
        // ****** top price spot 가격 설정 ****** //
        NSDictionary *topRankPriceData = allTicker.recentSpotTicker[topRankPriceExchange][asset][paymentCurrency];
        // Array에서 사용할 UILabel 가져오기
        UILabel *cryptoPriceHighLabel = _cryptoPriceHighLabelList[i];
        UILabel *changePricePercent24HighLabel = _changePricePercent24HighLabelList[i];
        // 데이터 출력하기
        if (topRankPriceData) {
            // 데이터가 정상적으로 있는 경우
            // 최신 가격
            NSString *price = topRankPriceData[@"price"];
            cryptoPriceHighLabel.text = price;
            
            // ****** top price spot 최근 24시간 가격 변동률 ****** //
            // 24시간 가격 변동률
            NSString *changePricePercent24 = [topRankPriceData[@"changePricePercent24"] stringByAppendingString:@"%"];
            changePricePercent24HighLabel.text = changePricePercent24;
            // 24시간 가격 변동률에 색 입히거나, 없는 경우에 대한 노출 경우의 수 처리
            if ([changePricePercent24 isEqual:@"-%"]) {
                // pass = defaul Color로 text 색칠하고, 기존 % 없는 -로 출력하기, 대표적으로 Kraken 거래소는 최근 24시간 가격 변동률을 구할 수 없어서 해당 기능 넣었음, 하지만 Kraken 아예 수집 안하는거로 수정해서 필요한 내용은 아님
                changePricePercent24HighLabel.text = @" - ";
            } else if ([changePricePercent24 hasPrefix:@"+"]) {
                // 음수가 아니라 0 또는 양수인 경우
                if ([topRankPriceData[@"changePricePercent24"] floatValue] == 0) {
                    // 0.00이면 보합
                    if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                        // 다크모드인 경우 글자색 흰색으로 원복
                        changePricePercent24HighLabel.textColor = [UIColor whiteColor];
                        cryptoPriceHighLabel.textColor = [UIColor whiteColor];
                    } else {
                        // 라이트모드인 경우 글자색 흑색으로 원복
                        changePricePercent24HighLabel.textColor = [UIColor blackColor];
                        cryptoPriceHighLabel.textColor = [UIColor blackColor];
                    }
                } else {
                    // 상승은 상승색
                    changePricePercent24HighLabel.textColor = [UIColor greenColor];
                    cryptoPriceHighLabel.textColor = [UIColor greenColor];
                }
            } else {
                // 하락은 하락색
                changePricePercent24HighLabel.textColor = [UIColor redColor];
                cryptoPriceHighLabel.textColor = [UIColor redColor];
            }
        } else {
            // ****** top price spot 데이터가 없는 경우 ****** //
            // 비정상 상태는 아니고, 0개의 거래소에 상장되어있는 경우 데이터가 아예 없어서 이런 현상 발생 + 이 경우에는 변동률 정보도 당연히 없음
            cryptoPriceHighLabel.text = @" - ";
            changePricePercent24HighLabel.text = @" - ";
        }
        
        // ****** bottom price spot 거래소 이미지 설정 ****** //
        UIImageView *exchangeLogoLowImage = _exchangeLogoLowImageList[i];
        exchangeLogoLowImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"information"][@"image"]];
        // ****** bottom price spot 가격 설정 ****** //
        NSDictionary *bottomRankPriceData = allTicker.recentSpotTicker[bottomRankPriceExchange][asset][paymentCurrency];
        // Array에서 사용할 UILabel 가져오기
        UILabel *cryptoPriceLowLabel = _cryptoPriceLowLabelList[i];
        UILabel *changePricePercent24LowLabel = _changePricePercent24LowLabelList[i];
        if (bottomRankPriceData[@"price"]) {
            // 데이터가 정상적으로 있는 경우
            cryptoPriceLowLabel.text = bottomRankPriceData[@"price"];
            NSString *changePricePercent24;
            if (bottomRankPriceData[@"changePricePercent24"]) {
                changePricePercent24 = [bottomRankPriceData[@"changePricePercent24"] stringByAppendingString:@"%"];
                changePricePercent24LowLabel.text = changePricePercent24;
                if ([changePricePercent24 hasPrefix:@"+"]) {
                    // 음수가 아닌 0이거나 양수인 경우
                    if ([bottomRankPriceData[@"changePricePercent24"] floatValue] == 0) {
                        // 0인 경우 기본색 = 0.00이면 보합
                        if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                            // 다크모드인 경우 글자색 흰색으로 원복
                            changePricePercent24LowLabel.textColor = [UIColor whiteColor];
                            cryptoPriceLowLabel.textColor = [UIColor whiteColor];
                        } else {
                            // 라이트모드인 경우 글자색 흑색으로 원복
                            changePricePercent24LowLabel.textColor = [UIColor blackColor];
                            cryptoPriceLowLabel.textColor = [UIColor blackColor];
                        }
                    } else {
                        // 양수인 경우
                        changePricePercent24LowLabel.textColor = [UIColor greenColor];
                        cryptoPriceLowLabel.textColor = [UIColor greenColor];
                    }
                } else {
                    // 음수인 경우
                    changePricePercent24LowLabel.textColor = [UIColor redColor];
                    cryptoPriceLowLabel.textColor = [UIColor redColor];
                }
            } else {
                changePricePercent24LowLabel.text = @" - %";
                NSLog(@"[WARN] changePricePercent24LowLabel error : %@", allTicker.recentSpotTicker[bottomRankPriceExchange][asset]);
            }
        } else {
            // 데이터가 없는 경우 : 비정상 상태는 아니고, 1개의 거래소에만 상장되어있는 경우 데이터가 없어서 이런 현상 발생
            cryptoPriceLowLabel.text = @" - ";
            changePricePercent24LowLabel.text = @" - ";
        }
        
        /* #################### 프리미엄 비교 #################### */
        // data를 float로 빼서 사용하기
        float topPrice;
        float bottomPrice;
        if (topRankPriceData[@"price"]) {
            // topRankPriceExchange 관련 정보 있는 경우에만 처리
            topPrice = [topRankPriceData[@"price"] floatValue];
        } else {
            // 없으면 1로!
            topPrice = 1;
        }
        if (bottomRankPriceData[@"price"]) {
            // bottomRankPriceExchange 관련 정보 있는 경우에만 처리
            bottomPrice = [bottomRankPriceData[@"price"] floatValue];
        } else {
            // 없으면 1로!
            bottomPrice = 1;
        }
        // 소수점 둘째자리 수까지
        NSString *maxPremiumPercent = [NSString stringWithFormat:@"%.2f", (topPrice-bottomPrice)/bottomPrice*100];
        // ****** max premium spot 이미지 및 프리미엄 수치 설정 ****** //
        if ([maxPremiumPercent isEqual:@"inf"]) {
            // 가격 비교할 거래소가 없어 1개의 거래소 데이터만 있는 경우
            maxPremiumPercent = @" - % ";
        } else if ([maxPremiumPercent isEqual:@"nan"]) {
            // 아무런 거래소에도 상장되어있지 않은 경우
            maxPremiumPercent = @" - % ";
        } else {
            // 가격 비교할 거래소가 있어 2개 이상의 거래소 데이터가 있는 일반적인 경우
            maxPremiumPercent = [maxPremiumPercent stringByAppendingString:@"%"];
        }
        
        // 특성 적용
        UILabel *maxPricePremiumPercentLabel = _maxPricePremiumPercentLabelList[i];
        // 특정 값 이상 프리미엄 발생시, 텍스트에 배경색 입히기
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:maxPremiumPercent];
        if ([maxPremiumPercent floatValue] >= 0.1) {
            // 0.1 이상인 경우 주황색
            [attributedString addAttribute:NSBackgroundColorAttributeName
                                     value:[UIColor orangeColor]
                                     range:NSMakeRange(0, maxPremiumPercent.length)];
        } else if ([maxPremiumPercent floatValue] >= 0.05) {
            // 0.05 이상인 경우 회색
            if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                // ****** 시스템 테마 설정에 따라 색 다르게 적용 ****** //
                // 다크모드인 경우
                [attributedString addAttribute:NSBackgroundColorAttributeName
                                         value:[UIColor darkGrayColor]
                                         range:NSMakeRange(0, maxPremiumPercent.length)];
            } else {
                // 라이트모드인 경우
                [attributedString addAttribute:NSBackgroundColorAttributeName
                                         value:[UIColor lightGrayColor]
                                         range:NSMakeRange(0, maxPremiumPercent.length)];
                
            }
        } else {
            // 일반 상태의 경우 clearColor로 지정해, 기존 색을 삭제
            [attributedString addAttribute:NSBackgroundColorAttributeName
                                     value:[UIColor clearColor]
                                     range:NSMakeRange(0, maxPremiumPercent.length)];
        }
        
        maxPricePremiumPercentLabel.attributedText = attributedString;
        
        UIImageView *maxPremiumTabHighExchangeImage = _maxPremiumTabHighExchangeImageList[i];
        UIImageView *maxPremiumTabLowExchangeImage = _maxPremiumTabLowExchangeImageList[i];
        maxPremiumTabHighExchangeImage.image = [UIImage imageNamed: [DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"information"][@"image"]];
        maxPremiumTabLowExchangeImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"information"][@"image"]];
        
        // **************************************** Futures & Spot 괴리율 disparity ratio 1,2등 **************************************** //
        /* #################### 데이터 들어갈 위치랑 Label, View 선언 및 세팅 #################### */
        // ****** 괴리울 1위 UILabel, UIImageView 가져오기 ****** //
        UILabel *cryptoPriceFuturesFirstLabel = _cryptoPriceFuturesFirstLabelList[i];
        UILabel *changePercent24FuturesFirstLabel = _changePercent24FuturesFirstLabelList[i];
        UIImageView *futuresFirstExchangeImage = _futuresFirstExchangeImageList[i];
        // ****** Futures & Spot 괴리울 2위 UILabel, UIImageView 가져오기 ****** //
        UILabel *cryptoPriceFuturesLastLabel = _cryptoPriceFuturesLastLabelList[i];
        UILabel *changePercent24FuturesLastLabel = _changePercent24FuturesLastLabelList[i];
        UIImageView *futuresLastExchangeImage = _futuresLastExchangeImageList[i];
        
        /* #################### First, Last 정보들 찾아내기 #################### */
        // ****** 사용할 변수들 선언해두기! Futures & Spot 괴리율 1,2등 거래소 정보 저장용 데이터 + 각 거래소의 index 찾을때 필요한 변수 선언 ****** //
        NSString *disparityRatioFirstRankExchange;
        NSString *disparityRatioLastRankExchange;
        startIndex = 1;
        NSUInteger firstRankIndex = 0;
        NSUInteger lastRankIndex = 0;
        // 괴리가격, 괴리율 정보 계산해서 데이터셋 만들기
        NSMutableDictionary *disparityData = [@{} mutableCopy];
        // 해당 symbol이 상장되어있는 거래소 개수 counting하기
        aliveSymbolCount = 0;
        for (int i=0; i<exchangeListFutures.count; i++) {
            if ([allTicker.recentFuturesTicker[exchangeListFutures[i]] objectForKey:asset]) {
                // 특정 자산 있는지 확인
                if ([allTicker.recentFuturesTicker[exchangeListFutures[i]][asset] objectForKey:paymentCurrency]) {
                    // 특정 지불 화폐 있는지 확인
                    aliveSymbolCount++;
                }
            }
        }
        if (aliveSymbolCount == 0) {
            // 해당 symbol이 상장된 거래소 정보를 찾는 로딩 중이거나 거래소 정보가 없는 경우 미노출
            // pass
        } else {
            // 1개 이상의 상장된 거래소가 발견될 경우
            // ****** rank를 매기기 위해 disparity ratio 계산하기 ****** //
            for (NSUInteger i=0; i<exchangeListFutures.count; i++) {
                if ([exchangeListSpot containsObject:exchangeListFutures[i]]) {
                    // 계산하려는 Futures 거래소를 Spot에서 취급하고 있는지 확인
                    if (allTicker.recentSpotTicker[exchangeListFutures[i]]) {
                        // 취급하려는 Futures exchange가 Spot Ticker에도 있는지 확인
                        if (allTicker.recentSpotTicker[exchangeListFutures[i]]) {
                            // exchange에 대해서
                            NSDictionary *priceOfFuturesData = allTicker.recentFuturesTicker[exchangeListFutures[i]];
                            NSDictionary *priceOfSpotData = allTicker.recentSpotTicker[exchangeListFutures[i]];
                            
                            NSString *futuresPrice = priceOfFuturesData[asset][paymentCurrency][@"price"];
                            NSString *spotPrice = priceOfSpotData[asset][paymentCurrency][@"price"];
                            float disparityPrice = [futuresPrice floatValue] - [spotPrice floatValue];
                            float disparityRatio = disparityPrice / [spotPrice floatValue] * 100;
                            disparityData[exchangeListFutures[i]] = @{
                                @"disparityPrice": [NSString stringWithFormat:@"%f", disparityPrice],
                                @"disparityRatio": [NSString stringWithFormat:@"%f", disparityRatio],
                            };
                        }
                    }
                }
            }
            
            // ****** first, last rank index 찾기 실행 ****** //
            // first index 먼저 찾기 = 가장 큰 값 찾기
            for (NSUInteger i=0; i<exchangeListFutures.count; i++) {
                // firstRankIndex 배정 전에, 정상적인 유효 거래소 price의 index를 일단 찾아서 firstRankIndex로 넣고 그거랑 비교 진행
                if (firstRankIndex != startIndex && allTicker.recentFuturesTicker[exchangeListFutures[i]][asset][paymentCurrency][@"price"]) {
                    // firstRankIndex랑 startIndex가 다른, 초기 상태이면서 price가 null이 아닌 유효한 값을 가질 때에만 진행, 만약 if문 안에 들어온다면 firstRankIndex와 startIndex, maxIndex가 같아지면서 해당 로직 미진행
                    // 목표는, 처음으로 값이 존재하는 exchange를 찾아 해당 거래소부터 대소비교를 진행하기 위함임!
                    startIndex = i;
                    firstRankIndex = i;
                    lastRankIndex = i;
                }
            }
            for (NSUInteger i=0; i<exchangeListFutures.count; i++) {
                if (startIndex == i) {
                    // first, last index 찾을때, startIndex는 어차피 firstRankIndex에서 가져가면서 체크하기때문에 체크 미진행
                    // pass
                } else {
                    // first index 찾는 거 진행
                    if (disparityData[exchangeListFutures[i]]) {
                        // index가 깊어서, 해당 깊이의 index가 존재하는지 확인한 후 대소 비교 진행하도록 if 조건문 실행 -> 만약 없으면 그냥 pass!
                        if (disparityData[exchangeListFutures[i]][@"disparityRatio"]) {
                            if (disparityData[exchangeListFutures[i]][@"disparityPrice"]) {
                                // 값이 없는 거래소의 경우 대소비교 미진행을 위한 예외처리
                                // 아예 수집을 하지 못한 거래소가 최소값인 거래소로 나오지 않도록, 없는 값은 아닌지 확인! null을 floatValue 하면 0.000000 이 나오기 때문에 무조건 작은 index로 잡힙니다. null인 애는 제외하고 대소비교를 해야합니다.
                                if ([disparityData[exchangeListFutures[i]][@"disparityRatio"] floatValue] > [disparityData[exchangeListFutures[firstRankIndex]][@"disparityRatio"] floatValue]) {
                                    // 가장 비싼 Exchange index 찾기
                                    firstRankIndex = i;
                                } else if ([disparityData[exchangeListFutures[i]][@"disparityRatio"] floatValue] < [disparityData[exchangeListFutures[lastRankIndex]][@"disparityRatio"] floatValue]) {
                                    // 가장 싼 Exchange index 찾기
                                    lastRankIndex = i;
                                } else {
                                    // 같은 경우에는 뒤쪽 array에 배치된 거래소를 lastRank로 배정
                                    lastRankIndex = i;
                                }
                            }
                        }
                    }
                }
            }
            
            // 거래소가 1개 뿐인 경우에는, first rank와 last rank가 같은 정보를 가지고 있습니다.
            disparityRatioFirstRankExchange = exchangeListFutures[firstRankIndex];
            disparityRatioLastRankExchange = exchangeListFutures[lastRankIndex];
        }
        
        /* #################### 데이터 출력해주기 #################### */
        // ****** first rank 거래소 정보 설정 ****** //
        futuresFirstExchangeImage.image = [UIImage imageNamed: [DefaultLoader sharedInstance].exchangeInfo[disparityRatioFirstRankExchange][@"information"][@"image"]];
        // ****** first Rank 가격 설정 ****** //
        NSDictionary *firstRankDisparityRatioData = allTicker.recentFuturesTicker[disparityRatioFirstRankExchange][asset][paymentCurrency];
        // 데이터 출력하기
        if (firstRankDisparityRatioData) {
            // 데이터가 정상적으로 있는 경우
            // 최신 가격
            NSString *price = firstRankDisparityRatioData[@"price"];
            cryptoPriceFuturesFirstLabel.text = price;
            // ****** first Rank에서 해당 Futures의 최근 24시간 가격 변동률 ****** //
            // 24시간 가격 변동률
            NSString *changePricePercent24 = [firstRankDisparityRatioData[@"changePricePercent24"] stringByAppendingString:@"%"];
            changePercent24FuturesFirstLabel.text = changePricePercent24;
            // 24시간 가격 변동률에 색 입히거나, 없는 경우에 대한 노출 경우의 수 처리
            if ([changePricePercent24 isEqual:@"-%"]) {
                // pass = defaul Color로 text 색칠하고, 기존 % 없는 -로 출력하기, 대표적으로 Kraken 거래소는 최근 24시간 가격 변동률을 구할 수 없어서 해당 기능 넣었음, 하지만 Kraken 아예 수집 안하는거로 수정해서 필요한 내용은 아님
            } else if ([changePricePercent24 hasPrefix:@"+"]) {
                // 음수가 아니라 0 또는 양수인 경우
                if ([firstRankDisparityRatioData[@"changePricePercent24"] floatValue] == 0) {
                    // 0.00이면 보합
                    if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                        // 다크모드인 경우 글자색 흰색으로 원복
                        changePercent24FuturesFirstLabel.textColor = [UIColor whiteColor];
                    } else {
                        // 라이트모드인 경우 글자색 흑색으로 원복
                        changePercent24FuturesFirstLabel.textColor = [UIColor blackColor];
                    }
                } else {
                    // 상승은 상승색
                    changePercent24FuturesFirstLabel.textColor = [UIColor greenColor];
                }
            } else {
                // 하락은 하락색
                changePercent24FuturesFirstLabel.textColor = [UIColor redColor];
            }
        } else {
            // ****** first Rank에서 해당 Futures의 데이터가 없는 경우 ****** //
            // 비정상 상태는 아니고, 0개의 거래소에 상장되어있는 경우 데이터가 아예 없어서 이런 현상 발생 + 이 경우에는 변동률 정보도 당연히 없음
            // pass
        }
        
        // ****** disparity ratio - last rank 거래소 정보 설정 ****** //
        NSDictionary *lastRankDisparityRatioData;
        // 거래소가 1개만 있는 경우에는 first rank 정보와 last rank 정보가 같으므로 노출X
        if (! disparityRatioLastRankExchange) {
            // 해당하는 거래소가 없는 경우
            // pass
        } else {
            // 거래소 정보가 있다면
            futuresLastExchangeImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[disparityRatioLastRankExchange][@"information"][@"image"]];
            // ****** disparity ratio - last rank 가격 설정 ****** //
            lastRankDisparityRatioData = allTicker.recentFuturesTicker[disparityRatioLastRankExchange][asset][paymentCurrency];
        }
        if (lastRankDisparityRatioData[@"price"]) {
            // 데이터가 정상적으로 있는 경우
            NSString *price = lastRankDisparityRatioData[@"price"];
            cryptoPriceFuturesLastLabel.text = price;
            // ****** disparity ratio - first Rank에서 해당 Futures의 최근 24시간 가격 변동률 ****** //
            // 24시간 가격 변동률
            NSString *changePricePercent24;
            if (lastRankDisparityRatioData[@"changePricePercent24"]) {
                changePricePercent24 = [lastRankDisparityRatioData[@"changePricePercent24"] stringByAppendingString:@"%"];
                changePercent24FuturesLastLabel.text = changePricePercent24;
                if ([changePricePercent24 hasPrefix:@"+"]) {
                    // 음수가 아닌 0이거나 양수인 경우
                    if ([lastRankDisparityRatioData[@"changePricePercent24"] floatValue] == 0) {
                        // 0인 경우 기본색 = 0.00이면 보합
                        if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                            // 다크모드인 경우 글자색 흰색으로 원복
                            changePercent24FuturesLastLabel.textColor = [UIColor whiteColor];
                        } else {
                            // 라이트모드인 경우 글자색 흑색으로 원복
                            changePercent24FuturesLastLabel.textColor = [UIColor blackColor];
                        }
                    } else {
                        // 양수인 경우
                        changePercent24FuturesLastLabel.textColor = [UIColor greenColor];
                    }
                } else {
                    // 음수인 경우
                    changePercent24FuturesLastLabel.textColor = [UIColor redColor];
                }
            } else {
                changePercent24FuturesLastLabel.text = @" - %";
                NSLog(@"[WARN] recentFuturesTicker[disparityRatioLastRankExchange][asset] error : %@", allTicker.recentFuturesTicker[disparityRatioLastRankExchange][asset]);
            }
        } else {
            // 데이터가 없는 경우 : 비정상 상태는 아니고, 1개의 거래소에만 상장되어있는 경우 데이터가 없어서 이런 현상 발생
            // pass
        }
        
        if ( ! [disparityData isEqual:@{}]) {
            // ****** Futures & Spot 비교 프리미엄 ****** //
            // first Rank
            NSString *firstRankPremiumPercent;
            if ([disparityData objectForKey:disparityRatioFirstRankExchange]) {
                UILabel *disparityRatioFirstRankLabel = _disparityRatioFirstRankLabelList[i];
                // 괴리율 노출
                NSString *disparityRatio = [NSString stringWithFormat:@"%.2f", [disparityData[disparityRatioFirstRankExchange][@"disparityRatio"] floatValue]];
                if ([disparityRatio hasPrefix:@"-"]) {
                    // 음수인 경우
                    firstRankPremiumPercent = disparityRatio;
                    disparityRatioFirstRankLabel.textColor = [UIColor redColor];
                } else {
                    firstRankPremiumPercent = [@"+" stringByAppendingString:disparityRatio];
                    if ([firstRankPremiumPercent floatValue] == 0) {
                        // 0인 경우에는 색 원복
                        if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                            // 다크모드인 경우 글자색 흰색으로 원복
                            disparityRatioFirstRankLabel.textColor = [UIColor whiteColor];
                        } else {
                            // 라이트모드인 경우 글자색 흑색으로 원복
                            disparityRatioFirstRankLabel.textColor = [UIColor blackColor];
                        }
                    } else {
                        // 양수인 경우
                        disparityRatioFirstRankLabel.textColor = [UIColor greenColor];
                    }
                }
                disparityRatioFirstRankLabel.text = [firstRankPremiumPercent stringByAppendingString:@"%"];
            }
            // last rank
            NSString *lastRankPremiumPercent;
            // 거래소가 2개 이상 있는 경우이므로 둘 다 노출
            UILabel *disparityRatioLastRankLabel = _disparityRatioLastRankLabelList[i];
            // 괴리율 노출
            NSString *disparityRatio = [NSString stringWithFormat:@"%.2f", [disparityData[disparityRatioLastRankExchange][@"disparityRatio"] floatValue]];
            if ([disparityRatio hasPrefix:@"-"]) {
                // 음수인 경우
                lastRankPremiumPercent = disparityRatio;
                disparityRatioLastRankLabel.textColor = [UIColor redColor];
            } else {
                // 양수이거나 0인 경우
                lastRankPremiumPercent = [@"+" stringByAppendingString:disparityRatio];
                if ([lastRankPremiumPercent floatValue] == 0) {
                    // 0인 경우에는 색 원복
                    if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                        // 다크모드인 경우 글자색 흰색으로 원복
                        disparityRatioLastRankLabel.textColor = [UIColor whiteColor];
                    } else {
                        // 라이트모드인 경우 글자색 흑색으로 원복
                        disparityRatioLastRankLabel.textColor = [UIColor blackColor];
                    }
                } else {
                    // 양수인 경우
                    disparityRatioLastRankLabel.textColor = [UIColor greenColor];
                }
            }
            disparityRatioLastRankLabel.text = [lastRankPremiumPercent stringByAppendingString:@"%"];
        }
    }
    // **************************************** [End] 카드뷰 목록 쭉 만들기 **************************************** //
}
@end

 

 

 

 

 

6. 결과 예시

 

 

 

+ Recent posts