1. 이전 포스팅 확인하기

 

https://growingsaja.tistory.com/914

 

 

 

 

 

2. 1시간마다 업데이트되는 환율 정보 return하는 api 서비스 제공 사이트 추천

 

 - Open Exchange Generates : https://openexchangerates.org/account/usage

 

    회원가입 후 로그인을 한 뒤, API KEY를 받아서 사용합니다.

 

 

 

 

 

3. Api 사용해보기

 

https://openexchangerates.org/api/latest.json?app_id={API_KEY}

 

매 시간 00분마다 업데이트된 환율 정보를 return합니다.

일반적으로 00분 00초 ~ 00분 30초 사이의 정보로 업데이트되는 것 같습니다.

 

 

 - 이외에 환율 정보 수집 가능 사이트

https://finance.naver.com/marketindex/exchangeDetail.naver?marketindexCd=FX_USDKRW 

 

https://finance.naver.com/marketindex/exchangeDetail.naver?marketindexCd=FX_USDKRW

1,273.00 원 전일대비 3.00 (+ 0.24% ) 2023.07.14 23:10 하나은행 고시회차 778회 1개월 3개월 1년 3년 5년 10년 환율계산기 (매매기준율 기준) 미국 달러 기준 대한민국 원 기준 관련 뉴스 인도·UAE, 자국 통화

finance.naver.com

 

 

 

 

 

4. default.json 파일 수정하기

 

api의 url 및 key값을 파일에 저장해둡니다.

 

{
    "popularList_description": [
        ["category-exchangeName", "Fullname", "codeName", "unit", "searchName", "isAvailable", "remark"],
        ["카테고리-거래소이름", "전체이름", "코드 또는 축약이름", "단위", "검색명", "활성화 여부", "비고"]
    ],
    "popularList": [
        [["spot-Bybit", "spot-Binance"], "Bitcoin", "BTC", "USDT", "BTCUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Ethereum", "ETH", "USDT", "ETHUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Cardano", "ADA", "USDT", "ADAUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Binance Coin", "BNB", "USDT", "BNBUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Solana", "SOL", "USDT", "SOLUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Ripple", "XRP", "USDT", "XRPUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Polkadot", "DOT", "USDT", "DOTUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Dogecoin", "DOGE", "USDT", "DOGEUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Chainlink", "LINK", "USDT", "LINKUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Cosmos", "ATOM", "USDT", "ATOMUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Polygon", "MATIC", "USDT", "MATICUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Stellar", "XLM", "USDT", "XLMUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Litecoin", "LTC", "USDT", "LTCUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Algorand", "ALGO", "USDT", "ALGOUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Aave", "AAVE", "USDT", "AAVEUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Filecoin", "FIL", "USDT", "FILUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Klaytn", "KLAY", "USDT", "KLAYUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Terra", "LUNA", "USDT", "LUNAUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Uniswap", "UNI", "USDT", "UNIUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Wrapped Bitcoin", "WBTC", "USDT", "WBTCUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Internet Computer", "ICP", "USDT", "ICPUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Zilliqa", "ZIL", "USDT", "ZILUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "OMG Network", "OMG", "USDT", "OMGUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Tezos", "XTZ", "USDT", "XTZUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Shiba Inu", "SHIB", "USDT", "SHIBUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Elrond", "EGLD", "USDT", "EGLDUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Axie Infinity", "AXS", "USDT", "AXSUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Decentraland", "MANA", "USDT", "MANAUSDT", "Y", ""],
        [["spot-Bybit", "spot-Binance"], "Waves", "WAVES", "USDT", "WAVESUSDT", "Y", ""]

    ],
    "exchangeImage": {
        "Binance": "exchangeBinance.png",
        "Bybit": "exchangeBybit.jpeg"
    },
    "externalApi": {
        "openExchangeRates":  {
            "key": "402fi9jk4389jg3408gu3j408gj350",
            "url": "https://openexchangerates.org/api/latest.json"
        }
    }
}

 

 

 

 

 

5. 프로젝트 구조 설명

 

  a. ServiceRates 파일 : 최신 환율 관련 데이터를 제공하는 자체적인 앱 내 구현 서비스입니다. 본 포스팅에서는 OpenExchangeRates를 통한 환율 정보만 수집하여 활용하지만, 추후 다른 api들로도 환율 정보를 수집해 그 중 가장 최신 정보를 노출할 예정입니다.

  b. OpenExchangeRates 파일 : OpenExchangeRates 서비스의 api로 환율 정보를 수집합니다. 단순하게 해당 api에 콜해서 데이터를 return받는 기능만 구현합니다.

 

 - 주의사항으로는, b인 OpenExchangeRates에서 구현한 기능을 계속 사용할 경우 api call limit에 너무 빨리 도달할 수 있기에, a인 ServiceRates에서 콜 횟수 및 주기 조절 조건 if문을 구현할 예정이며, 이를 위해 isNeedFirstTry를 property 변수로 BOOL 형태 선언하여 init과 함께 TRUE로 설정했다가, 처음으로 api call을 통한 데이터 수집이 성공하면 FALSE로 변경하면서 수집 시도를 하지 않도록 하기 위한 용도로 사용합니다.

 - 하지만 OpenExchangeRates의 경우 매시 00분 00초 혹은 그로부터 조금 지난 후에 해당 시점의 환율 정보를 return해주기 때문에 00분 30초에 b인 OpenExchangeRates에 있는 data fetch 기능을 다시 실행하여 콜을 진행합니다.

 

 본 기능과 관련된 모든 기능의 경우 싱글톤으로 구현합니다. 과거 데이터와의 비교가 추후 필요할 수 있고, 환율 데이터의 경우 대부분의 무료 api는 실시간 정보를 얻기 힘들기 때문에 추후 다른 api들과 융합해 구현할 것을 고려해 싱글톤으로 구현할 것을 추천합니다.

 즉, 다른 기능들과 함께 유기적으로 작동할 필요가 있을 것으로 예상되므로 싱글톤으로 구현합니다.

 

 

 

 

 

6. OpenExchangeRates 파일 작성

 

// vim OpenExchangeRates.h

@interface OpenExchangeRates : NSObject

@property (readwrite, strong, nonatomic) NSString *dateTime;
@property (readwrite, strong, nonatomic) NSNumber *unixTimestamp;
@property (readwrite, strong, nonatomic) NSNumber *usdkrw;
@property (readwrite, strong, nonatomic) NSString *base;


@property (readwrite, strong, nonatomic) NSString *beforeDateTime;
@property (readwrite, strong, nonatomic) NSNumber *beforeUnixTimestamp;
@property (readwrite, strong, nonatomic) NSNumber *beforeUsdkrw;
@property (readwrite, strong, nonatomic) NSString *beforeBase;


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

@end

 

// vim OpenExchangeRates.h

@interface OpenExchangeRates : NSObject

@property (readwrite, strong, nonatomic) NSString *dateTime;
@property (readwrite, strong, nonatomic) NSNumber *unixTimestamp;
@property (readwrite, strong, nonatomic) NSNumber *usdkrw;
@property (readwrite, strong, nonatomic) NSString *base;


@property (readwrite, strong, nonatomic) NSString *beforeDateTime;
@property (readwrite, strong, nonatomic) NSNumber *beforeUnixTimestamp;
@property (readwrite, strong, nonatomic) NSNumber *beforeUsdkrw;
@property (readwrite, strong, nonatomic) NSString *beforeBase;


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

@end

 

 

 

 

 

7. ServiceRecentRates 파일 작성

 

// 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 *usdkrwLabel;

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

@property (strong, nonatomic) NSMutableArray *cryptoPriceHighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeNameHighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoHighImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceLowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeNameLowLabelList; // 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

-(void) updateCardData;
-(void) loadPopularList;
-(void) setPriceList;
-(void) updateView;

@end

 

// vim ServiceRecentRates.m

#import <Foundation/Foundation.h>
#import "ServiceRecentRates.h"
// 외부 api 구현 부문 빼둔 클래스 사용
#import "OpenExchangeRates.h"
// 외부 api call 조건식에 현재 시간이 들어가서 사용
#import "CRTDateTime.h"

@implementation ServiceRecentRates

+(instancetype) sharedInstance {
    
    static ServiceRecentRates *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
        sharedInstance.isNeedFirstTry = TRUE;
    });
    return sharedInstance;
}
 
-(void)getData {
    // 최신 데이터 찾기위해 잠시 넣어두는 List 용도
    NSMutableArray *usdkrwList = [@[] mutableCopy];
    NSMutableArray *datetimeList = [@[] mutableCopy];
    NSMutableArray *unixTimestampList = [@[] mutableCopy];
    NSMutableArray *baseList = [@[] mutableCopy];
    // 최신 데이터 찾기위해 잠시 넣어두는 List 용도
    NSMutableArray *beforeUsdkrwList = [@[] mutableCopy];
    NSMutableArray *beforeDatetimeList = [@[] mutableCopy];
    NSMutableArray *beforeUnixTimestampList = [@[] mutableCopy];
    NSMutableArray *beforeBaseList = [@[] mutableCopy];
    
    /* #################### index 0 : Open Exchange Rates 데이터 업데이트 #################### */
    OpenExchangeRates *usdkrwInstance0 = [OpenExchangeRates sharedInstance];
    // ****** Open Exchange Rates api 콜 조건 구현 ****** //
    // -- 계속 콜하면 api 콜 limit 걸립니다. 현재 1개월에 1,000회가 무료이니 거기까지만 쓰는 것을 추천하며 00분 00초 ~ 30초 사이마다 1시간 단위로 데이터가 최신화되는 점을 감안했습니다.
    NSString *nowOnlyMinSecondTime = [[[CRTDateTime alloc] init] NowUTC: @"mm:ss"];
    if (_isNeedFirstTry || [nowOnlyMinSecondTime isEqual:@"00:30"]) {
        // 매 시의 00분 30초에만 api 콜 진행, 본 코드는 매 1초마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwInstance0 fetchDataWithCompletion];
        _isNeedFirstTry = FALSE;
    }
    // ****** index 0 : Open Exchange Rates 최신 데이터 업데이트 ****** //
    // 환율
    if (usdkrwInstance0.usdkrw) {
        usdkrwList[0] = usdkrwInstance0.usdkrw;
        beforeUsdkrwList[0] = usdkrwInstance0.usdkrw;
    } else {
        usdkrwList[0] = @0;
        beforeUsdkrwList[0] = @0;
    }
    // 업데이트 시간
    if (usdkrwInstance0.dateTime && usdkrwInstance0.unixTimestamp) {
        datetimeList[0] = usdkrwInstance0.dateTime;
        unixTimestampList[0] = usdkrwInstance0.unixTimestamp;
        beforeDatetimeList[0] = usdkrwInstance0.dateTime;
        beforeUnixTimestampList[0] = usdkrwInstance0.unixTimestamp;
    } else {
        datetimeList[0] = @"🕜";
        unixTimestampList[0] = @0;
        beforeDatetimeList[0] = @"🕜";
        beforeUnixTimestampList[0] = @0;
    }
    // 기준 화폐
    if (usdkrwInstance0.base) {
        baseList[0] = usdkrwInstance0.base;
        beforeBaseList[0] = usdkrwInstance0.base;
    } else {
        baseList[0] = @"💱";
        beforeBaseList[0] = @"💱";
    }
    // ****** index 0 : Open Exchange Rates Last 후보 데이터 업데이트 ****** //
    // 환율   0:이전/1:최신
    if (usdkrwInstance0.beforeUsdkrw) {
        beforeUsdkrwList[1] = usdkrwInstance0.beforeUsdkrw;
    } else {
        beforeUsdkrwList[1] = @0;
    }
    // 업데이트 시간   0:이전/1:최신
    if (usdkrwInstance0.beforeDateTime && usdkrwInstance0.beforeUnixTimestamp) {
        beforeDatetimeList[1] = usdkrwInstance0.beforeDateTime;
        beforeUnixTimestampList[1] = usdkrwInstance0.beforeUnixTimestamp;
    } else {
        beforeDatetimeList[1] = @"🕜";
        beforeUnixTimestampList[1] = @0;
    }
    // 기준 화폐   0:이전/1:최신
    if (usdkrwInstance0.beforeBase) {
        beforeBaseList[1] = usdkrwInstance0.beforeBase;
    } else {
        beforeBaseList[1] = @"💱";
    }
    
    /* #################### All index 비교 #################### */
    // 일단 api 1개니까 1개만 그대로 넣는데, 추후에는 최근 업데이트값을 찾아서 넣을 것
    // ****** 최신 데이터 = 최신 데이터들 중 가장 최근 날짜거 ****** //
    _usdkrw = usdkrwList[0];
    _dateTime = datetimeList[0];
    _base = baseList[0];
    _unixTimestamp = unixTimestampList[0];
    // ****** 직전 데이터 = 최신 데이터를 제외하고 가장 최근 날짜거 ****** //
    _beforeUsdkrw = beforeUsdkrwList[1];
    _beforeDateTime = beforeDatetimeList[1];
    _beforeBase = beforeBaseList[1];
    _beforeUnixTimestamp = beforeUnixTimestampList[1];
}

@end

 

 

 

 

 

8. 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 *exchangeNameHighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoHighImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceLowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeNameLowLabelList; // 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

-(void) updateCardData;
-(void) loadPopularList;
-(void) setPriceList;
-(void) updateView;

@end

 

// vim Controller/PopluarAssetListVC.m

#import <Foundation/Foundation.h>
#import "PopluarAssetListVC.h"
// api 통신 용도
#import "APIManager.h"
// default.json 데이터 읽기
#import "DefaultLoader.h"
// 현재 시간 가져오는 용도
#import "CRTDateTime.h"
// 환율 서비스
#import "ServiceRecentRates.h"

@implementation PopluarAssetListVC {
    // json에서 가져온 popularList raw 데이터
    NSArray *popularList;
    // Spot 가격 정보 저장용 데이터
    NSMutableDictionary *popularSpotPriceList;
}

- (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의 popularList 데이터 가져오기
    [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/3, 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/3, 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.exchangeNameHighLabelList = [@[] mutableCopy];
    self.exchangeLogoHighImageList = [@[] mutableCopy];
    self.cryptoPriceLowLabelList = [@[] mutableCopy];
    self.exchangeNameLowLabelList = [@[] 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 / 2, basicMarginInCard, cardViewWidth / 2, 20)];
        cryptoPriceHighLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceHighLabel.font = [UIFont fontWithName:@"Pretendard-Bold" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 이름을 위한 기본 셋
        UILabel *exchangeNameHighLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 4 + miniImange, basicMarginInCard, cardViewWidth/4, 20)];
        exchangeNameHighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoHighImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 4, basicMarginInCard, miniImange, miniImange)];
        exchangeLogoHighImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### Low spot 현재 가격, 거래소 정보 노출 라벨 세팅 #################### */
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceLowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 2, cardViewHeight/2, cardViewWidth / 2, 20)];
        cryptoPriceLowLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceLowLabel.font = [UIFont fontWithName:@"Pretendard-Bold" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 이름을 위한 기본 셋
        UILabel *exchangeNameLowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 4 + miniImange, cardViewHeight/2, cardViewWidth/4, 20)];
        exchangeNameLowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoLowImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 4, 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-Bold" 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:exchangeNameHighLabel];
        [_exchangeNameHighLabelList addObject:exchangeNameHighLabel];
        [cardView addSubview:exchangeLogoHighImage];
        [_exchangeLogoHighImageList addObject:exchangeLogoHighImage];
        
        // bottom rank spot
        [cardView addSubview:cryptoPriceLowLabel];
        [_cryptoPriceLowLabelList addObject:cryptoPriceLowLabel];
        [cardView addSubview:exchangeNameLowLabel];
        [_exchangeNameLowLabelList addObject:exchangeNameLowLabel];
        [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 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
    [NSTimer scheduledTimerWithTimeInterval:1
                                     target:self
                                   selector:@selector(updateCardData)
                                   userInfo:nil
                                    repeats:YES];
}

// 데이터 가져오기 및 뷰 업데이트 코드
- (void)updateCardData {
    
    // **************************************** [Start] api 콜 준비 **************************************** //
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Binance 데이터 가져오기 **************************************** //
    NSString *apiURL = @"https://api.binance.com/api/v3/ticker/price";
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            // Binance는 api return data가 array
            NSArray* resultOfApi = (NSArray *)jsonResponse;
            
            // api로 받은 데이터 깔끔한 dictionary로 가공하기
            for (int i=0; i<resultOfApi.count; i++) {
                NSString *symbol = resultOfApi[i][@"symbol"];
                NSString *price = resultOfApi[i][@"price"];
                if ([price floatValue] >= 1000) {
                    price = [price substringWithRange:NSMakeRange(0, price.length-6)];
                } else if ([price floatValue] >= 100) {
                    price = [price substringWithRange:NSMakeRange(0, price.length-5)];
                } else if ([price floatValue] >= 1) {
                    price = [price substringWithRange:NSMakeRange(0, price.length-4)];
                } else if ([price floatValue] >= 0.1) {
                    price = [price substringWithRange:NSMakeRange(0, price.length-3)];
                } else if ([price floatValue] >= 0.01) {
                    price = [price substringWithRange:NSMakeRange(0, price.length-2)];
                } else if ([price floatValue] >= 0.001) {
                    price = [price substringWithRange:NSMakeRange(0, price.length-1)];
                } else {
                    // price값 그대로 가져다씁니다.
                }

                self->popularSpotPriceList[@"Binance"][symbol] = price;
            }
        }
    }];
    // **************************************** [End] Binance 데이터 가져오기 **************************************** //
    // **************************************** [Start] Bybit 데이터 가져오기 **************************************** //
    apiURL = @"https://api.bybit.com/spot/quote/v1/ticker/price";
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            // result 안의 value만 추출
            NSArray* resultOfApi = jsonResponse[@"result"];
            
            // api로 받은 데이터 깔끔한 dictionary로 가공하기
            for (int i=0; i<resultOfApi.count; i++) {
                NSString *symbol = resultOfApi[i][@"symbol"];
                NSString *price = resultOfApi[i][@"price"];
                self->popularSpotPriceList[@"Bybit"][symbol] = price;
            }
        }
    }];
    // **************************************** [End] Bybit 데이터 가져오기 **************************************** //
    // 메인 스레드에서만 UI 업데이트 수행
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateView];
    });
}

// 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 {
    
    // 레이블의 텍스트를 설정합니다. 여기에서는 UTC 시간을 업데이트합니다.
    self.liveUTCLabel.text = [[[CRTDateTime alloc] init] NowUTC: @"yyyy-MM-dd (E) HH:mm:ss"];
    
    // 레이블의 텍스트를 설정합니다. 여기에서는 KST 시간을 업데이트합니다.
    self.liveKSTLabel.text = [[[CRTDateTime alloc] init] NowKST: @"yyyy-MM-dd (E) HH:mm:ss"];
    
    // USDKRW 환율 정보 업데이트
    [[ServiceRecentRates sharedInstance] getData];
    NSNumber *usdkrw = [ServiceRecentRates sharedInstance].usdkrw;
    NSNumber *beforeUsdkrw = [ServiceRecentRates sharedInstance].beforeUsdkrw;
    self.ratesLabel.text = [usdkrw stringValue];
    self.beforeRatesLabel.text = [beforeUsdkrw stringValue];
    
    // **************************************** [Start] 카드뷰 데이터 업데이트 **************************************** //
    for (int i=0; i<popularList.count; i++) {
        // 기준 key인 symbol 변수 설정
        NSString *symbolKey = popularList[i][4];
        /* #################### 카드뷰 기본 세팅 #################### */
        UILabel *cryptoNameLabel = _cryptoNameLabelList[i];
        cryptoNameLabel.text = symbolKey;
        
        /* #################### 취급 상품 정리 및 데이터 가공 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 = @"";
        // 가격 정보가 1개라도 있을 경우
        if ([popularSpotPriceList[exchangeNameList[0]] objectForKey:symbolKey] || [popularSpotPriceList[exchangeNameList[1]] objectForKey:symbolKey]) {
            if ([popularSpotPriceList[exchangeNameList[0]][symbolKey] floatValue] >= [popularSpotPriceList[exchangeNameList[1]][symbolKey] floatValue]) {
                // index 0번 거래소가 1번 거래소보다 더 크거나 같으면
                topRankPriceExchange = exchangeNameList[0];
                bottomRankPriceExchange = exchangeNameList[1];
            } else {
                // index 1번 거래소가 0번 거래소보다 더 크면
                topRankPriceExchange = exchangeNameList[1];
                bottomRankPriceExchange = exchangeNameList[0];
            }
        } else {
            // 가격 정보가 모두 없을 경우 (로딩중이거나 못불러오거나) 전부 0으로 노출
            topRankPriceExchange = exchangeNameList[0];
            bottomRankPriceExchange = exchangeNameList[1];
            popularSpotPriceList[topRankPriceExchange][symbolKey] = 0;
            popularSpotPriceList[bottomRankPriceExchange][symbolKey] = 0;
        }
        
        // ****** top price spot 거래소명 및 이미지 설정 ****** //
        ((UILabel *)_exchangeNameHighLabelList[i]).text = topRankPriceExchange;
        ((UIImageView *)_exchangeLogoHighImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[topRankPriceExchange]];;
        ((UILabel *)_cryptoPriceHighLabelList[i]).text = popularSpotPriceList[topRankPriceExchange][symbolKey];
        
        // ****** bottom price spot 거래소 이미지 설정 ****** //
        ((UILabel *)_exchangeNameLowLabelList[i]).text = bottomRankPriceExchange;
        ((UIImageView *)_exchangeLogoLowImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[bottomRankPriceExchange]];
        ((UILabel *)_cryptoPriceLowLabelList[i]).text = popularSpotPriceList[bottomRankPriceExchange][symbolKey];
        
        /* #################### 프리미엄 비교 #################### */
        float topPrice = [popularSpotPriceList[topRankPriceExchange][symbolKey] floatValue];
        float bottomPrice = [popularSpotPriceList[bottomRankPriceExchange][symbolKey] floatValue];
        // 소수점 둘째자리 수까지
        NSString *maxPremiumPercent = [NSString stringWithFormat:@"%.2f", (topPrice-bottomPrice)/bottomPrice*100];
        
        // ****** max premium spot 이미지 및 프리미엄 수치 설정 ****** //
        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].exchangeImage[topRankPriceExchange]];
        ((UIImageView *)_maxPremiumTabLowExchangeImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[bottomRankPriceExchange]];
        
    }
    // **************************************** [End] 카드뷰 목록 쭉 만들기 **************************************** //
    
}
@end

 

 

 

 

 

9. 결과 예시

 

 

 

 

 

 

+ Recent posts