1. 이전 포스팅 확인하기

 

https://growingsaja.tistory.com/928

 

[Objective-C] 앱 만들기 입문 - 27 : 3개 이상의 거래소 정보 노출에 대한 처리 기능 추가 및 다른 거

1. 이전 포스팅 확인하기 https://growingsaja.tistory.com/927 2. 이번 목표 symbol 기준으로 Binance, Bybit 이외에 다른 거래소 api를 사용해서 최대, 최소인 거래소 찾아내어 노출해주는 기능으로 업그레이드하

growingsaja.tistory.com

 

 

 

 

 

2. 이번 목표

 

okx exchange api로 ticker 가져와 앱서비스 노출 데이터에 정보 추가하기

 

 

 

 

 

3. default.json 데이터 추가

 

        "Okx": {
            "image": "exchangeOkx.jpeg",
            "keyList": [],
            "baseUrlList": [
                "https://www.okx.com",
                "https://aws.okx.com"
            ],
            "publicApi": {
                "ticker": "/api/v5/market/tickers",
                "orderbook": ""
            },
            "spot": {
                "stable": {
                    "category": "spot",
                    "paymentCurrencyList": ["USDT", "USDC", "DAI"]
                },
                "bitcoin": {
                    "category": "spot",
                    "paymentCurrencyList": ["BTC"]
                },
                "alt": {
                    "category": "spot",
                    "paymentCurrencyList": ["ETH", "OKB", "DOT", "EURT"]
                },
                "fiat": {
                    "category": "spot",
                    "paymentCurrencyList": ["EUR", "USD", "GBP"]
                }
            },
            "futures": {
                "stableMargin" : {
                    "category": "SWAP",
                    "paymentCurrencyList": ["USDT", "USDC"]
                },
                "coinMargin": {
                    "category": "SWAP",
                    "paymentCurrencyList": ["USD"]
                }
            }
        },

 

 

 

 

 

4. SpotOkx 파일 생성 및 작성

 

// vim SpotOkx.h

@interface SpotOkx : NSObject

// symbol 기준으로 현재 실시간 정보 얻기
@property (readwrite, strong, nonatomic) NSMutableDictionary *dataFromSymbol;

// asset, payment currency 기준으로 현재 실시간 정보 얻기
@property (readwrite, strong, nonatomic) NSMutableDictionary *dataFromAsset;

// payment currency 정보
@property (readwrite, strong, nonatomic) NSMutableDictionary *paymentCurrencyInfo;


+(instancetype)sharedInstance;
-(void) fetchDataWithCompletion;

@end

 

// vim SpotOkx.m

#import <Foundation/Foundation.h>
#import "SpotOkx.h"
// 주요 데이터 get
#import "DefaultLoader.h"
// api 연결
#import "APIManager.h"

@implementation SpotOkx

+(instancetype)sharedInstance {
    static SpotOkx *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
        sharedInstance.dataFromSymbol = [@{} mutableCopy];
        sharedInstance.dataFromAsset = [@{} mutableCopy];
        
        sharedInstance.paymentCurrencyInfo = [@{
            @"spot": [@[] mutableCopy],
            @"futures": [@[] mutableCopy]
        } mutableCopy];
        // payment currency 데이터 참조해서 symbol 찢기 용도 데이터셋 선언
        [sharedInstance getOnlyPaymentCurrencyData];
    });
    return sharedInstance;
}

-(void) fetchDataWithCompletion {
    // **************************************** [Start] api 콜 준비 **************************************** //
    NSDictionary *okx = [DefaultLoader sharedInstance].exchangeInfo[@"Okx"];
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Bybit 현재 호가, 변동률 등 주요 데이터 가져오기 **************************************** //
    NSString *apiURL = [okx[@"baseUrlList"][0] stringByAppendingString:okx[@"publicApi"][@"ticker"]];
    apiURL = [apiURL stringByAppendingString:@"?instType=SPOT"];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"[Error] %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            if ([jsonResponse[@"code"] isEqual:@"0"]) {
                // ****** 정상 성공시 ****** //
                // 기본적으로 code, msg, requestTime이라는 키값을 가지며, main으로는 data라는 키값을 가지는데, 그 안에 list로 상품별 정보를 가집니다. (requestTime은 unix timestamp입니다.)
                // result 안의 value만 추출
                NSArray* resultOfApi = jsonResponse[@"data"];
                // api로 받은 데이터 깔끔한 dictionary로 가공하기
                for (int i=0; i<resultOfApi.count; i++) {
                    /* #################### dataFromSymbol #################### */
                    // ****** api의 데이터들 수집한거 필요시 가공해서 저장하기 ****** //
                    // OKX거래소의 경우 dataFromAsset을 먼저 선언하는게 더 좋다! ex. iinstId : BTC-USDT / BTC-USD-SWAP
                    NSString *asset = [[resultOfApi[i][@"instId"] componentsSeparatedByString:@"-"] objectAtIndex:0];
                    NSString *paymentCurrency = [[resultOfApi[i][@"instId"] componentsSeparatedByString:@"-"] objectAtIndex:1];
                    NSString *symbol = [asset stringByAppendingString:paymentCurrency]; // 심볼
                    NSString *price = resultOfApi[i][@"last"]; // 가격
                    // 변동률은 데이터를 다른 거래소 정보와 함께 노출시 통일성있게 하기 위해 부분 가공이 진행됩니다.
                    float diffPrice = [price floatValue] - [resultOfApi[i][@"open24h"] floatValue];
                    NSString *changePricePercent24 = [NSString stringWithFormat:@"%.2f", (diffPrice) / [resultOfApi[i][@"open24h"] floatValue] * 100]; // 근24시간 가격 변동률
                    if (![changePricePercent24 hasPrefix:@"-"]) {
                        // 음수가 아닌 경우, +로 기호 맨앞에 붙여주기
                        changePricePercent24 = [@"+" stringByAppendingString:changePricePercent24];
                    }
                    NSString *volume24 = resultOfApi[i][@"vol24h"]; // 근24시간 거래량
                    NSString *turnover24 = resultOfApi[i][@"volCcy24h"]; // 근24시간 거래대금
                    // dataFromSymbol
                    self.dataFromSymbol[symbol] = [@{} mutableCopy];
                    self.dataFromSymbol[symbol][@"price"] = price; // 가격
                    self.dataFromSymbol[symbol][@"changePricePercent24"] = changePricePercent24; // 근24시간 가격 변동률
                    self.dataFromSymbol[symbol][@"turnover24"] = turnover24; // 근24시간 거래대금
                    self.dataFromSymbol[symbol][@"volume24"] = volume24; // 근24시간 거래량
                    /* #################### dataFromAsset #################### */
                    // 사용된 payment currency 찾고 symbol을 찢기
                    for (NSString *eachpaymentCurrency in self.paymentCurrencyInfo[@"spot"]) {
                        // 뒤에 payment currency 매치되는거 찾은 경우 분리 진행
                        if ([symbol hasSuffix:eachpaymentCurrency]) {
                            asset = [[symbol componentsSeparatedByString:eachpaymentCurrency] objectAtIndex:0];
                            paymentCurrency = eachpaymentCurrency;
                        }
                    }
                    // dataFromAsset 데이터셋 만들어주고
                    if (asset && paymentCurrency) {
                        // 취급하는 payment currency인 경우
                        self.dataFromAsset[asset] = [@{} mutableCopy];
                        self.dataFromAsset[asset][paymentCurrency] = [@{} mutableCopy];
                        // dataFromAsset 데이터셋 만들어주고
                        self.dataFromAsset[asset][paymentCurrency][@"price"] = price; // 가격
                        self.dataFromAsset[asset][paymentCurrency][@"changePricePercent24"] = changePricePercent24; // 근24시간 가격 변동률
                        self.dataFromAsset[asset][paymentCurrency][@"turnover24"] = turnover24; // 근24시간 거래대금
                        self.dataFromAsset[asset][paymentCurrency][@"volume24"] = volume24; // 근24시간 거래량
                    } else {
                        // 취급하지 않는 payment currency인 경우
                        // pass합니다. 그래도 뭐가 skip되는지는 알아야하니까 1번만 출력합니다.
                        static dispatch_once_t oneTokenTmp;
                        dispatch_once(&oneTokenTmp, ^{
                            NSLog(@"[WARN] not include : %@", symbol);
                        });
                    }
                }
            } else {
                // ****** 비정상으로 실패시 ****** //
                // 에러가 발생한 경우 retCode는 0이 아니며, retMsg에 관련 상세 내용이 있습니다.
                NSLog(@"[ERROR] Return Code : %@", jsonResponse[@"code"]);
                NSLog(@"[ERROR] Return Message : %@", jsonResponse[@"msg"]);
                // api로 받은 데이터 깔끔한 dictionary로 가공하기
            }
        }
    }];
}


// _paymentCurrencyInfo : spot, futures 각각에서 취급하는 paymentCurrencyList를 보유하게 됩니다. 해당 정보를 토대로, symbol 데이터를 활용해 asset과 paymentCurrency를 분리할 수 있습니다.
-(void) getOnlyPaymentCurrencyData {
    // bybit 관련 default 데이터 불러오기
    NSDictionary *okx = [DefaultLoader sharedInstance].exchangeInfo[@"Okx"];
    NSArray *categoryList = @[@"spot", @"futures"];
    // 데이터 정리하기
    for (NSString *category in categoryList) {
        for (NSString *group in [okx[category] allKeys]) {
            if ([okx[category][group][@"category"] isEqual:category]) {
                // spot
                [_paymentCurrencyInfo[category] addObjectsFromArray:okx[category][group][@"paymentCurrencyList"]];
            } else if ([okx[category][group][@"category"] isEqual:@"coin-m"]) {
                // usd
                [_paymentCurrencyInfo[category] addObjectsFromArray:okx[category][@"coinMargin"][@"paymentCurrencyList"]];
            } else if ([okx[category][group][@"category"] isEqual:@"usd(s)-m"]) {
                // usdt
                [_paymentCurrencyInfo[category] addObjectsFromArray:okx[category][@"stableMargin"][@"paymentCurrencyList"]];
            } else {
                NSLog(@"[WARN] Bybit spot payment currency not match : Skip...");
            }
        }
    }
}






@end

 

 

 

 

 

5. PopluarAssetListVC 파일 수정 주요 소스코드

 

// vim PopularAssetListVC.m



// ...




    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 뷰 그리기 0.5초에 1번씩
    [NSTimer scheduledTimerWithTimeInterval:0.8
                                     target:self
                                   selector:@selector(updateCardDataOkx)
                                   userInfo:nil
                                    repeats:YES];
    
    
    
// ...




// 데이터 가져오기 코드
- (void)updateCardDataOkx {
    // **************************************** [Start] Bitget 데이터 가져오기 **************************************** //
    // api로 받은 데이터 깔끔한 dictionary로 가공하기
    SpotOkx *mainData = [SpotOkx sharedInstance];
    [mainData fetchDataWithCompletion];
    self.popularSpotPriceList[@"Okx"] = mainData.dataFromSymbol;
    
    // **************************************** [End] Bitget 데이터 가져오기 **************************************** //
}



// ...

 

 

 

 

 

 

 

6. 결과 예시

 

 

 

 

 

 

7. PopluarAssetListVC 파일 수정 전체 소스코드

 

// 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) UILabel *ratesLabel;
@property (strong, nonatomic) UILabel *beforeRatesLabel;

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

@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

/* #################### 데이터 세팅 #################### */
// default.json의 exchangeInfo 저장
@property (strong, nonatomic) NSDictionary *exchangeInfo;
// 현재 가격 정보 저장하는 데이터
// Spot 가격 정보 저장용 데이터
@property (readwrite, strong, nonatomic) NSMutableDictionary *popularSpotPriceList;

/* #################### 메소드 #################### */
// 거래소별로 api call을 통한 데이터 업데이트 주기가 다르게 설정되어 분리
-(void) updateCardDataBinance;
-(void) updateCardDataBybit;

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

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

-(void) loadPopularList;
-(void) setPriceList;


@end

 

// vim Controller/PopluarAssetListVC.m

#import <Foundation/Foundation.h>
#import "PopluarAssetListVC.h"
// api 통신 용도
#import "APIManager.h"
// default.json 데이터 읽기
#import "DefaultLoader.h"
// 현재 시간 가져오는 용도
#import "DateTime.h"
// unix 변환용
#import "CRTunixToString.h"
// 환율 서비스
#import "ServiceRecentRates.h"
// 거래소 api
#import "SpotBybit.h"
#import "SpotBinance.h"
#import "SpotBitget.h"
#import "SpotOkx.h"
//#import "SpotKraken.h"    // 2023-07-24 Kraken 미사용으로 변경 (because 최근 24시간 가격 변동률 정보 추출 불가)

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

- (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];
    [self setPriceList];
    
    // 데이터를 표시할 레이블 생성
    // **************************************** [Start] 뷰 그리기 **************************************** //
    //카드뷰 배치에 필요한 변수를 설정합니다.
    // 카드 목록 나열될 공간 세팅
    // ****************************** //
    // 목록 상단 공백 margin 설정
    CGFloat cardViewTopStartMargin = 10.0;
    // 카드 목록의 좌우 공백 각각의 margin
    CGFloat horizontalMargin = 2.0;
    // 카드와 카드 사이의 공백
    CGFloat cardViewSpacing = 0.0;
    // 카드뷰 목록 노출 시작 x축 위치 자동 설정
    CGFloat cardViewXPosition = horizontalMargin;
    // ****************************** //
    // 카드 자체에 대한 세팅
    // 카드 높이 길이 (상하 길이) 설정
    CGFloat cardViewHeight = 44.0;
    // 카드 좌우 길이 phone size 참조하여 자동 조정
    CGFloat cardViewWidth = [UIScreen mainScreen].bounds.size.width - horizontalMargin * 2;
    // 카드뷰 안에 내용 들어가는 공간까지의 margin
    CGFloat basicMarginInCard = 4.0;
    CGFloat defaultFontSize = 16.0;
    // ****************************** //
    
    // **************************************** [Start] 최상단에 기타 정보 공간 **************************************** //
    /* #################### 현재 시간 정보 #################### */
    self.topInfoTab = [[UIView alloc] initWithFrame:CGRectMake(cardViewXPosition, cardViewTopStartMargin - (cardViewSpacing + cardViewHeight), cardViewWidth, cardViewHeight)];
    // 국기 너비 길이 (좌우 길이) 설정
    CGFloat miniImange = 18.0;
    
    // ****** 글로벌 지구 이모티콘 및 시간 설정 ****** //
    self.earthLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard, miniImange, 20)];
    self.earthLabel.text = @"🌍";
    // 표준 시간 = 그리니치 표준시 : 더블린, 에든버러, 리스본, 런던, 카사블랑카, 몬로비아
    self.liveUTCLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + miniImange, basicMarginInCard, cardViewWidth / 2, 20)];
    self.liveUTCLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    // ****** 한국 이모티콘 및 시간 설정 ****** //
    // 태극기
    self.koreanFlagLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard + cardViewHeight / 2, miniImange, 20)];
    self.koreanFlagLabel.text = @"🇰🇷";
    // 한국 시간
    self.liveKSTLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + miniImange, basicMarginInCard + cardViewHeight / 2, cardViewWidth / 2, 20)];
    self.liveKSTLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    /* #################### 환율 정보 #################### */
    self.ratesLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/3*2, basicMarginInCard, cardViewWidth/4, 20)];
    self.ratesLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesLabel.textAlignment = NSTextAlignmentRight;
    self.beforeRatesLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/3*2, basicMarginInCard + cardViewHeight / 2, cardViewWidth/4, 20)];
    self.beforeRatesLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.beforeRatesLabel.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:_ratesLabel];
    [self.topInfoTab addSubview:_beforeRatesLabel];
    
    // ****** 상단 UIView를 self.scrollView에 추가 ****** //
    [self.scrollView addSubview:_topInfoTab];
    
    
    // **************************************** [Start] 카드뷰 목록 쭉 만들기 **************************************** //
    self.cardViewList = [@[] mutableCopy];
    self.cryptoNameLabelList = [@[] mutableCopy];
    self.cryptoPriceHighLabelList = [@[] mutableCopy];
    self.changePricePercent24HighLabelList = [@[] mutableCopy];
    self.exchangeLogoHighImageList = [@[] mutableCopy];
    self.cryptoPriceLowLabelList = [@[] mutableCopy];
    self.changePricePercent24LowLabelList = [@[] mutableCopy];
    self.exchangeLogoLowImageList = [@[] mutableCopy];
    self.maxPricePremiumPercentLabelList = [@[] mutableCopy];
    self.maxPremiumTabHighExchangeImageList = [@[] mutableCopy];
    self.maxPremiumTabLowExchangeImageList = [@[] mutableCopy];
    
    for (int i=0; i<popularList.count; i++) {
        /* #################### 카드뷰 기본 세팅 #################### */
        UIView *cardView = [[UIView alloc] initWithFrame:CGRectMake(cardViewXPosition, cardViewTopStartMargin + i * (cardViewSpacing + 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(basicMarginInCard, basicMarginInCard, cardViewWidth / 4, 20)];
        
        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 / 5, basicMarginInCard, cardViewWidth / 5, 20)];
        cryptoPriceHighLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceHighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePricePercent24HighLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 9, basicMarginInCard, cardViewWidth/3, 20)];
        changePricePercent24HighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoHighImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 2, basicMarginInCard, miniImange, miniImange)];
        exchangeLogoHighImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### Low spot 현재 가격, 거래소 정보 노출 라벨 세팅 #################### */
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceLowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5, cardViewHeight/2, cardViewWidth / 5, 20)];
        cryptoPriceLowLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceLowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePricePercent24LowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 9, cardViewHeight/2, cardViewWidth/3, 20)];
        changePricePercent24LowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoLowImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 2, cardViewHeight/2, miniImange, miniImange)];
        exchangeLogoLowImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### spot 프리미엄 노출 세팅 #################### */
        // 기본 셋
        UILabel *maxPricePremiumPercentLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, cardViewHeight/2, cardViewWidth / 8 + miniImange, 20)];
        maxPricePremiumPercentLabel.textAlignment = NSTextAlignmentRight;
        maxPricePremiumPercentLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        // top rank 거래소 아이콘을 위한 기본 셋
        UIImageView *maxPremiumTabHighExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(basicMarginInCard, cardViewHeight/2, miniImange, miniImange)];
        maxPremiumTabHighExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        // bottom rank 거래소 아이콘을 위한 기본 셋
        UIImageView *maxPremiumTabLowExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(basicMarginInCard + miniImange + cardViewWidth / 8, cardViewHeight/2, miniImange, miniImange)];
        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];
        
        // cardView를 self.scrollView에 추가합니다.
        [self.scrollView addSubview:cardView];
        // 레이블 세팅 완료된 cardView를 CardViewList에 넣기
        [self.cardViewList addObject: cardView];
    }
    
    // 상하 스크롤 최대치 자동 설정
    CGFloat contentHeight = popularList.count * (cardViewHeight + cardViewSpacing);
    self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, contentHeight);
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 바이낸스 2초에 1번씩
    [NSTimer scheduledTimerWithTimeInterval:2
                                     target:self
                                   selector:@selector(updateCardDataBinance)
                                   userInfo:nil
                                    repeats:YES];
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 바이비트 0.5초에 1번씩
    [NSTimer scheduledTimerWithTimeInterval:0.8
                                     target:self
                                   selector:@selector(updateCardDataBybit)
                                   userInfo:nil
                                    repeats:YES];
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 뷰 그리기 0.5초에 1번씩
    [NSTimer scheduledTimerWithTimeInterval:0.8
                                     target:self
                                   selector:@selector(updateCardDataBitget)
                                   userInfo:nil
                                    repeats:YES];
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 뷰 그리기 0.5초에 1번씩
    [NSTimer scheduledTimerWithTimeInterval:0.8
                                     target:self
                                   selector:@selector(updateCardDataOkx)
                                   userInfo:nil
                                    repeats:YES];
    
//    // ** 2023-07-24 Kraken 미사용으로 변경 (because 최근 24시간 가격 변동률 정보 추출 불가) ** //
//    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 뷰 그리기 0.5초에 1번씩
//    [NSTimer scheduledTimerWithTimeInterval:0.3
//                                     target:self
//                                   selector:@selector(updateCardDataKraken)
//                                   userInfo:nil
//                                    repeats:YES];
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출 - 뷰 그리기 0.5초에 1번씩
    [NSTimer scheduledTimerWithTimeInterval:0.3
                                     target:self
                                   selector:@selector(updateCardView)
                                   userInfo:nil
                                    repeats:YES];
}


// 데이터 가져오기 코드
- (void)updateCardDataBinance {
    // **************************************** [Start] Binance 데이터 가져오기 **************************************** //
    // api로 받은 데이터 깔끔한 dictionary로 가공하기
    SpotBinance *mainData = [SpotBinance sharedInstance];
    [mainData fetchDataWithCompletion];
    self.popularSpotPriceList[@"Binance"] = mainData.dataFromSymbol;
    // **************************************** [End] Binance 데이터 가져오기 **************************************** //
}

// 데이터 가져오기 코드
- (void)updateCardDataBybit {
    // **************************************** [Start] Bybit 데이터 가져오기 **************************************** //
    // api로 받은 데이터 깔끔한 dictionary로 가공하기
    SpotBybit *mainData = [SpotBybit sharedInstance];
    [mainData fetchDataWithCompletion];
    self.popularSpotPriceList[@"Bybit"] = mainData.dataFromSymbol;
    
    // **************************************** [End] Bybit 데이터 가져오기 **************************************** //
}

// 데이터 가져오기 코드
- (void)updateCardDataBitget {
    // **************************************** [Start] Bitget 데이터 가져오기 **************************************** //
    // api로 받은 데이터 깔끔한 dictionary로 가공하기
    SpotBitget *mainData = [SpotBitget sharedInstance];
    [mainData fetchDataWithCompletion];
    self.popularSpotPriceList[@"Bitget"] = mainData.dataFromSymbol;
    
    // **************************************** [End] Bitget 데이터 가져오기 **************************************** //
}

// 데이터 가져오기 코드
- (void)updateCardDataOkx {
    // **************************************** [Start] Bitget 데이터 가져오기 **************************************** //
    // api로 받은 데이터 깔끔한 dictionary로 가공하기
    SpotOkx *mainData = [SpotOkx sharedInstance];
    [mainData fetchDataWithCompletion];
    self.popularSpotPriceList[@"Okx"] = mainData.dataFromSymbol;
    
    // **************************************** [End] Bitget 데이터 가져오기 **************************************** //
}

//    // ** 2023-07-24 Kraken 미사용으로 변경 (because 최근 24시간 가격 변동률 정보 추출 불가) ** //
//// 데이터 가져오기 코드
//- (void)updateCardDataKraken {
//    // **************************************** [Start] Kraken 데이터 가져오기 **************************************** //
//    // api로 받은 데이터 깔끔한 dictionary로 가공하기
//    SpotKraken *mainData = [SpotKraken sharedInstance];
//    [mainData fetchDataWithCompletion];
//    self.popularSpotPriceList[@"Kraken"] = mainData.dataFromSymbol;
//
//    // **************************************** [End] Kraken 데이터 가져오기 **************************************** //
//}

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

// default.json 파일의 exchangeInfo 데이터 읽기
-(void) loadExchangeInfo {
    // default.json의 popularList 불러오고 정상인지 1차 확인하기 = popularList에 있는 각 element들의 개수가 같은지 확인
    _exchangeInfo = [DefaultLoader sharedInstance].exchangeInfo;
}

// default.json 파일의 popularList 데이터 읽기
-(void) loadPopularList {
    // default.json의 popularList 불러오고 정상인지 1차 확인하기 = popularList에 있는 각 element들의 개수가 같은지 확인
    popularList = [DefaultLoader sharedInstance].popularList;
    int checkDefaultJsonFile = 0;
    for (int i=0; i<popularList.count-1; i++) {
        if ([popularList[i] count] == [popularList[i+1] count]) {
            checkDefaultJsonFile += 1;
        }
    }
    if (checkDefaultJsonFile == [popularList count]-1) {
        // default.json파일의 popularList 안에 있는 Array들 중 길이가 모두 같으면
        NSLog(@"%@", @"[INFO] default.json Check : Normal");
    } else {
        // default.json파일의 popularList 안에 있는 Array들 중 길이가 다른 것이 1개라도 있으면 WARN 출력 및 앱 dead
        NSLog(@"****************************************************");
        NSLog(@"[WARN] Check File : default.json - popularList");
        NSLog(@"****************************************************");
    }
}

// popularList에서 수집을 1건 이상이라도 할 예정인 거래소 목록을 실시간 호가 정보 데이터에 셋업하기
-(void) setPriceList {
    // 초기값 세팅 (안해주면 for문 돌면서 해당 dictionary에 데이터가 들어가지 않음)
    _popularSpotPriceList = [@{} mutableCopy];
    for (int i=0; i<popularList.count; i++) {
        for (NSString *eachPopular in popularList[i][0]) {
            NSString *category = [[eachPopular componentsSeparatedByString:@"-"] objectAtIndex:0];
            NSString *exchange = [[eachPopular componentsSeparatedByString:@"-"] objectAtIndex:1];
            if ([category isEqual:@"spot"]) {
                _popularSpotPriceList[exchange] = [@{} mutableCopy];
            }
        }
    }
}

-(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];
    [ratesInfo getData];
    self.ratesLabel.text = [ratesInfo.usdkrw stringValue];
    self.beforeRatesLabel.text = [ratesInfo.beforeUsdkrw stringValue];
    
    // **************************************** [Start] 카드뷰 데이터 업데이트 **************************************** //
    for (int i=0; i<popularList.count; i++) {
        // 기준 key인 symbol 변수 설정
        NSString *symbol = popularList[i][4];
        /* #################### 카드뷰 기본 세팅 #################### */
        UILabel *cryptoNameLabel = _cryptoNameLabelList[i];
        cryptoNameLabel.text = symbol;
        
        /* #################### 취급 상품 정리 및 데이터 가공 for 동일 상품에 대한 거래소간 가격 비교 기존 Array 제작 #################### */
        NSMutableArray *exchangeNameList = [@[] mutableCopy];
        // 특정 거래소에 있는지 확인
        for (NSString *item in popularList[i][0]) {
            NSString *category = [[item componentsSeparatedByString:@"-"] objectAtIndex:0];
            // spot인 상품의 거래소 목록 만들기
            if ([category isEqual: @"spot"]) {
                NSString *exchangeName = [[item componentsSeparatedByString:@"-"] objectAtIndex:1];
                [exchangeNameList addObject:exchangeName];
            }
        }
        
        /* #################### 누가 High로 배치될지, Low로 배치될지 로직 #################### */
        // 거래소 가격 비교해보고 탑랭크, 바텀랭크 지정
        NSString *topRankPriceExchange = @"";
        NSString *bottomRankPriceExchange = @"";
        NSUInteger minIndex = 0;
        NSUInteger maxIndex = 0;
        if ([_popularSpotPriceList[exchangeNameList[0]] objectForKey:symbol] || [_popularSpotPriceList[exchangeNameList[1]] objectForKey:symbol] || [_popularSpotPriceList[exchangeNameList[2]] objectForKey:symbol]) {
            // 가격 정보가 1개 이상인 경우
            for (NSUInteger i=1; i<exchangeNameList.count; i++) {
                if (_popularSpotPriceList[exchangeNameList[i]][symbol][@"price"]) {
                    // 아예 수집을 하지 못한 거래소가 최소값인 거래소로 나오지 않도록, 없는 값은 아닌지 확인! null을 floatValue 하면 0.000000 이 나오기 때문에 무조건 작은 index로 잡힙니다. null인 애는 제외하고 대소비교를 해야합니다.
                    if ([_popularSpotPriceList[exchangeNameList[i]][symbol][@"price"] floatValue] <= [_popularSpotPriceList[exchangeNameList[minIndex]][symbol][@"price"] floatValue]) {
                        // 가장 저렴한 Exchange index 찾기
                        minIndex = i;
                    }
                    if ([_popularSpotPriceList[exchangeNameList[i]][symbol][@"price"] floatValue] > [_popularSpotPriceList[exchangeNameList[maxIndex]][symbol][@"price"] floatValue]) {
                        // 가장 비싼 Exchange index 찾기
                        maxIndex = i;
                    }
                }
            }
            // 찾은 최소 index와 최대 index를 통해 top, bottom 거래소명 값 저장
            topRankPriceExchange = exchangeNameList[maxIndex];
            bottomRankPriceExchange = exchangeNameList[minIndex];
        } else {
            // 가격 정보가 모두 없을 경우 (로딩중이거나 못불러오거나) 전부 0으로 노출
            topRankPriceExchange = exchangeNameList[1];
            bottomRankPriceExchange = exchangeNameList[1];
            _popularSpotPriceList[topRankPriceExchange][symbol][@"price"] = 0;
            _popularSpotPriceList[bottomRankPriceExchange][symbol][@"price"] = 0;
        }
        
        // ****** top price spot 거래소명 및 이미지 설정 ****** //
        ((UIImageView *)_exchangeLogoHighImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"image"]];;
        // top price 가격 확보 실패시
        if (_popularSpotPriceList[topRankPriceExchange][symbol][@"price"]) {
            // 데이터가 정상적으로 있는 경우
            ((UILabel *)_cryptoPriceHighLabelList[i]).text = _popularSpotPriceList[topRankPriceExchange][symbol][@"price"];
            NSString *changePricePercent24 = [_popularSpotPriceList[topRankPriceExchange][symbol][@"changePricePercent24"] stringByAppendingString:@"%"];
            ((UILabel *)_changePricePercent24HighLabelList[i]).text = changePricePercent24;
            if ([changePricePercent24 isEqual:@"-%"]) {
                // pass = defaul Color로 text 색칠하고, 기존 % 없는 -로 출력하기, 대표적으로 Kraken 거래소는 최근 24시간 가격 변동률을 구할 수 없어서 해당 기능 넣었음, 하지만 Kraken 아예 수집 안하는거로 수정해서 필요한 내용은 아님
                ((UILabel *)_changePricePercent24HighLabelList[i]).text = @" - ";
            } else if ([changePricePercent24 hasPrefix:@"+"]) {
                // 상승은 빨간색
                ((UILabel *)_changePricePercent24HighLabelList[i]).textColor = [UIColor redColor];
            } else {
                // 하락은 파란색
                ((UILabel *)_changePricePercent24HighLabelList[i]).textColor = [UIColor blueColor];
            }
        } else {
            // 데이터가 없는 경우 : 비정상 상태는 아니고, 0개의 거래소에 상장되어있는 경우 데이터가 아예 없어서 이런 현상 발생
            ((UILabel *)_cryptoPriceHighLabelList[i]).text = @" - ";
            ((UILabel *)_changePricePercent24HighLabelList[i]).text = @" - ";
        }
        
        // ****** bottom price spot 거래소 이미지 설정 ****** //
        ((UIImageView *)_exchangeLogoLowImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"image"]];
        // bottom price 가격 확보 실패시
        if (_popularSpotPriceList[bottomRankPriceExchange][symbol][@"price"]) {
            // 데이터가 정상적으로 있는 경우
            ((UILabel *)_cryptoPriceLowLabelList[i]).text = _popularSpotPriceList[bottomRankPriceExchange][symbol][@"price"];
            NSString *changePricePercent24 = [_popularSpotPriceList[bottomRankPriceExchange][symbol][@"changePricePercent24"] stringByAppendingString:@"%"];
            ((UILabel *)_changePricePercent24LowLabelList[i]).text = changePricePercent24;
            if ([changePricePercent24 hasPrefix:@"+"]) {
                ((UILabel *)_changePricePercent24LowLabelList[i]).textColor = [UIColor redColor];
            } else {
                ((UILabel *)_changePricePercent24LowLabelList[i]).textColor = [UIColor blueColor];
            }
        } else {
            // 데이터가 없는 경우 : 비정상 상태는 아니고, 1개의 거래소에만 상장되어있는 경우 데이터가 없어서 이런 현상 발생
            ((UILabel *)_cryptoPriceLowLabelList[i]).text = @" - ";
            ((UILabel *)_changePricePercent24LowLabelList[i]).text = @" - ";
        }
        
        /* #################### 프리미엄 비교 #################### */
        float topPrice = [_popularSpotPriceList[topRankPriceExchange][symbol][@"price"] floatValue];
        float bottomPrice = [_popularSpotPriceList[bottomRankPriceExchange][symbol][@"price"] floatValue];
        // 소수점 둘째자리 수까지
        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:@"%"];
        }
        
        // 특정 값 이상 프리미엄 발생시, 텍스트에 배경색 입히기
        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)];
        }
        
        // 특성 적용
        ((UILabel *)_maxPricePremiumPercentLabelList[i]).attributedText = attributedString;
        
        ((UIImageView *)_maxPremiumTabHighExchangeImageList[i]).image = [UIImage imageNamed: [DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"image"]];
        ((UIImageView *)_maxPremiumTabLowExchangeImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"image"]];
        
    }
    // **************************************** [End] 카드뷰 목록 쭉 만들기 **************************************** //
    
}
@end

 

 

 

+ Recent posts