1. 이전 포스팅 확인하기

 

https://growingsaja.tistory.com/931

 

[Objective-C] 앱 만들기 입문 - 28 : 다른 거래소 Okx 추가

1. 이전 포스팅 확인하기 https://growingsaja.tistory.com/928 [Objective-C] 앱 만들기 입문 - 27 : 3개 이상의 거래소 정보 노출에 대한 처리 기능 추가 및 다른 거 1. 이전 포스팅 확인하기 https://growingsaja.tistory

growingsaja.tistory.com

 

 

 

 

 

2. 이번 목표

 

 - default.json 데이터 형태 대폭 수정

 - exchange 정보를 default.json에서 관리하고, true/false로 spot/futures 활성화 여부 기능 추가

 - spot live 데이터 보관 기능 별도 파일로 분리

 - PopularAssetListVC 코드 리펙토링 및 개선 : ex. 거래소 여러개가 되더라도 알아서 탐지하고 뷰 그리는 기능 추가

 

 

 

 

 

3. default.json 변경

 

{
    "popularList_description": [
        ["category-exchangeName", "Fullname", "codeName", "unit", "searchName", "isAvailable", "remark"],
        ["카테고리-거래소이름", "전체이름", "코드 또는 축약이름", "단위", "검색명", "활성화 여부", "비고"]
    ],
    "popularList": [
        [[], "Bitcoin", "BTC", "USDT", "BTCUSDT", "Y", ""],
        [[], "Ethereum", "ETH", "USDT", "ETHUSDT", "Y", ""],
        [[], "Ripple", "XRP", "USDT", "XRPUSDT", "Y", ""],
        [[], "Dogecoin", "DOGE", "USDT", "DOGEUSDT", "Y", ""],
        [[], "Cardano", "ADA", "USDT", "ADAUSDT", "Y", ""],
        [[], "Solana", "SOL", "USDT", "SOLUSDT", "Y", ""],
        [[], "Polkadot", "DOT", "USDT", "DOTUSDT", "Y", ""],
        [[], "Chainlink", "LINK", "USDT", "LINKUSDT", "Y", ""],
        [[], "Litecoin", "LTC", "USDT", "LTCUSDT", "Y", ""],
        [[], "Tezos", "XTZ", "USDT", "XTZUSDT", "Y", ""],
        [[], "Binance Coin", "BNB", "USDT", "BNBUSDT", "Y", ""],
        [[], "Axie Infinity", "AXS", "USDT", "AXSUSDT", "Y", ""],
        [[], "Decentraland", "MANA", "USDT", "MANAUSDT", "Y", ""],
        [[], "Zilliqa", "ZIL", "USDT", "ZILUSDT", "Y", ""],
        [[], "Klaytn", "KLAY", "USDT", "KLAYUSDT", "Y", ""],
        [[], "Blur", "BLUR", "USDT", "BLURUSDT", "Y", ""],
        [[], "Waves", "WAVES", "USDT", "WAVESUSDT", "Y", ""],
        [[], "Shiba Inu", "SHIB", "USDT", "SHIBUSDT", "Y", ""],
        [[], "Polygon", "MATIC", "USDT", "MATICUSDT", "Y", ""],
        [[], "Stellar", "XLM", "USDT", "XLMUSDT", "Y", ""],
        [[], "Filecoin", "FIL", "USDT", "FILUSDT", "Y", ""],
        [[], "Cosmos", "ATOM", "USDT", "ATOMUSDT", "Y", ""],
        [[], "Terra", "LUNA", "USDT", "LUNAUSDT", "Y", ""],
        [[], "Algorand", "ALGO", "USDT", "ALGOUSDT", "Y", ""],
        [[], "Aave", "AAVE", "USDT", "AAVEUSDT", "Y", ""],
        [[], "Uniswap", "UNI", "USDT", "UNIUSDT", "Y", ""],
        [[], "Wrapped Bitcoin", "WBTC", "USDT", "WBTCUSDT", "Y", ""],
        [[], "Internet Computer", "ICP", "USDT", "ICPUSDT", "Y", ""],
        [[], "OMG Network", "OMG", "USDT", "OMGUSDT", "Y", ""],
        [[], "Elrond", "EGLD", "USDT", "EGLDUSDT", "Y", ""]
    ],
    "externalApi": {
        "OpenExchangeRates":  {
            "key": "kf0e29ik4jg9243j0f239jdl3gsg329",
            "url": "https://openexchangerates.org/api/latest.json"
        }
    },
    "exchangeInfo": {
        "Binance": {
            "isActive": {
                "spot": true,
                "futures": true
            },
            "information": {
                "image": "exchangeBinance.png",
                "keyList": [],
                "publicApi": {
                    "ticker": "/api/v3/ticker/24hr",
                    "orderbook": ""
                },
                "baseUrlList": [
                    "https://api.binance.com",
                    "https://api-gcp.binance.com"
                ]
            },
            "spot": {
                "stable": {
                    "category": "spot",
                    "paymentCurrencyList": ["USDT", "TUSD", "BUSD", "USDC", "DAI"]
                },
                "bitcoin": {
                    "category": "spot",
                    "paymentCurrencyList": ["BTC"]
                },
                "alt": {
                    "category": "spot",
                    "paymentCurrencyList": ["BNB", "ETH", "DOGE", "XRP", "VAI", "TRX", "DOT"]
                },
                "fiat": {
                    "category": "spot",
                    "paymentCurrencyList": ["TRY", "EUR", "BRL", "ARS", "BIDR", "GBP", "IDRT", "NGN", "PLN", "RON", "RUB", "UAH", "ZAR"]
                }
            },
            "futures": {
                "stableMargin" : {
                    "category": "usd(s)-m",
                    "paymentCurrencyList": ["USDT", "BUSD"]
                },
                "coinMargin": {
                    "category": "coin-m",
                    "paymentCurrencyList": ["USD"]
                }
            }
        },
        "Bybit": {
            "isActive": {
                "spot": true,
                "futures": true
            },
            "information": {
                "image": "exchangeBybit.jpeg",
                "keyList": [],
                "baseUrlList": [
                    "https://api.bybit.com",
                    "https://api.bytick.com"
                ],
                "publicApi": {
                    "ticker": "/v5/market/tickers",
                    "orderbook": ""
                }
            },
            "spot": {
                "stable": {
                    "category": "spot",
                    "paymentCurrencyList": ["USDT", "USDC", "DAI"]
                },
                "bitcoin": {
                    "category": "spot",
                    "paymentCurrencyList": ["BTC"]
                },
                "alt": {
                    "category": "spot",
                    "paymentCurrencyList": []
                },
                "fiat": {
                    "category": "spot",
                    "paymentCurrencyList": ["EUR"]
                }
            },
            "futures": {
                "stableMargin" : {
                    "category": "linear",
                    "paymentCurrencyList": ["USDT"]
                },
                "coinMargin": {
                    "category": "inverse",
                    "paymentCurrencyList": ["USD"]
                }
            }
        },
        "Bitget": {
            "isActive": {
                "spot": true,
                "futures": true
            },
            "information": {
                "image": "exchangeBitget.png",
                "keyList": [],
                "baseUrlList": [
                    "https://api.bitget.com"
                ],
                "publicApi": {
                    "ticker": "/api/spot/v1/market/tickers",
                    "orderbook": ""
                }
            },
            "spot": {
                "stable": {
                    "category": "spot",
                    "paymentCurrencyList": ["USDT", "USDC"]
                },
                "bitcoin": {
                    "category": "spot",
                    "paymentCurrencyList": ["BTC"]
                },
                "alt": {
                    "category": "spot",
                    "paymentCurrencyList": ["ETH"]
                },
                "fiat": {
                    "category": "spot",
                    "paymentCurrencyList": ["EUR", "RUB", "UAH", "BRL", "GBP"]
                }
            },
            "futures": {
                "stableMargin" : {
                    "category": "futures",
                    "paymentCurrencyList": []
                },
                "coinMargin": {
                    "category": "futures",
                    "paymentCurrencyList": []
                }
            }
        },
        "Okx": {
            "isActive": {
                "spot": false,
                "futures": false
            },
            "information": {
                "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"]
                }
            }
        },
        "Kraken": {
            "isActive": {
                "spot": false,
                "futures": false
            },
            "information": {
                "image": "exchangeKraken.png",
                "keyList": [],
                "baseUrlList": [
                    "https://api.kraken.com"
                ],
                "publicApi": {
                    "ticker": "/0/public/Ticker",
                    "orderbook": ""
                }
            },
            "spot": {
                "stable": {
                    "category": "spot",
                    "paymentCurrencyList": ["USDT", "USDC"]
                },
                "bitcoin": {
                    "category": "spot",
                    "paymentCurrencyList": ["BTC"]
                },
                "alt": {
                    "category": "spot",
                    "paymentCurrencyList": ["ETH"]
                },
                "fiat": {
                    "category": "spot",
                    "paymentCurrencyList": ["EUR", "USD", "GBP"]
                }
            },
            "futures": {
                "stableMargin" : {
                    "category": "futures",
                    "paymentCurrencyList": []
                },
                "coinMargin": {
                    "category": "futures",
                    "paymentCurrencyList": []
                }
            }
        },
        "Bithumb": {
            "isActive": {
                "spot": false,
                "futures": false
            },
            "information": {
                "keyList": [],
                "baseUrlList": [
                    "https://api.bithumb.com"
                ],
                "publicApi": {
                    "ticker": "/public/ticker/ALL_",
                    "orderbook": "/public/orderbook/ALL_"
                }
            },
            "spot": {
                "bitcoin": {
                    "category": "spot",
                    "paymentCurrencyList": ["BTC"]
                },
                "fiat": {
                    "category": "spot",
                    "paymentCurrencyList": ["KRW"]
                }
            },
            "futures": {}
        }
    }
}

 

 

 

 

 

4. DefaultLoader 파일 수정

 

// vim DefaultLoader.h

@interface DefaultLoader: NSObject

@property (readwrite, strong, nonatomic) NSArray *popularList;
@property (readwrite, strong, nonatomic) NSDictionary *exchangeImage;
@property (readwrite, strong, nonatomic) NSDictionary *externalApi;
@property (readwrite, strong, nonatomic) NSDictionary *exchangeInfo;
@property (readwrite, strong, nonatomic) NSDictionary *exchangeInfoAll;
@property (readwrite, strong, nonatomic) NSArray *exchangeList;


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

@end

 

// vim DefaultLoader.m

#import <Foundation/Foundation.h>
#import "DefaultLoader.h"
#import "CRTJSONReader.h"

@implementation DefaultLoader

+(instancetype)sharedInstance {
    static DefaultLoader *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[DefaultLoader alloc] init];
        // 설정 파일을 로드하는 코드를 작성하십시오.
        CRTJSONReader *tryReading = [CRTJSONReader sharedInstance];
        [tryReading tryReadJsonFile:@"default.json"];
        sharedInstance.popularList = tryReading.loadData[@"popularList"];
        sharedInstance.exchangeImage = tryReading.loadData[@"exchangeImage"];
        sharedInstance.externalApi = tryReading.loadData[@"externalApi"];
        sharedInstance.exchangeInfoAll = tryReading.loadData[@"exchangeInfo"];
        [sharedInstance fetchActiveExchangeInfo];
    });
    return sharedInstance;
}

-(void)fetchActiveExchangeInfo {
    // 모든 exchange 정보를 저장한 후, active가 아닌 데이터 삭제하는 기능 구현
    NSArray *exchangeList = [_exchangeInfoAll allKeys];
    NSMutableDictionary *exchangeInfoTmp = [_exchangeInfoAll mutableCopy];
    for (int i=0; i<exchangeList.count; i++) {
        if ( ! [_exchangeInfoAll[exchangeList[i]][@"isActive"][@"spot"] boolValue] && ! [_exchangeInfoAll[exchangeList[i]][@"isActive"][@"futures"] boolValue]) {
            // spot, futures 모두 비활성화인 경우 -> 해당 거래소 데이터 자체를 삭제
            [exchangeInfoTmp removeObjectForKey:exchangeList[i]];
        } else if (! [_exchangeInfoAll[exchangeList[i]][@"isActive"][@"spot"] boolValue]) {
            // spot만 비활성화인 경우 -> 해당 거래소의 spot 데이터만 삭제
            [exchangeInfoTmp[exchangeList[i]] removeObjectForKey:@"spot"];
        }
        if ( ! [_exchangeInfoAll[_exchangeList[i]][@"isActive"][@"futures"] boolValue]) {
            // futures만 비활성화인 경우 -> 해당 거래소의 futures 데이터만 삭제
            [exchangeInfoTmp[exchangeList[i]] removeObjectForKey:@"futures"];
        }
    }
    // exchangeTmp 세팅 완료시, exchangeInfo에 NSDictionary로 저장
    _exchangeInfo= [exchangeInfoTmp mutableCopy];
}

@end

 

 

 

 

 

5. AllSpotLiveData 파일 생성 및 작성

 

이제 AllSpotLiveData로 저장하여, 어디에서든 spot 데이터에 접근할 수 있습니다.

 

// vim AllSpotLiveData.h

@interface AllSpotLiveData : NSObject

// ****** 현재 가격 정보 저장하는 데이터 ****** //
// 최신 Spot 가격 정보 저장용 데이터
@property (readwrite, strong, nonatomic) NSMutableDictionary *recentPriceData;


+(instancetype) sharedInstance;

@end

 

// vim AllSpotLiveData.m

#import <Foundation/Foundation.h>
#import "AllSpotLiveData.h"
#import "DefaultLoader.h"

@implementation AllSpotLiveData

+(instancetype)sharedInstance {
    static AllSpotLiveData *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
        // 초기값 세팅 (안해주면 for문 돌면서 해당 dictionary에 데이터가 들어가지 않음)
        sharedInstance.recentPriceData = [@{} mutableCopy];
        // exchange Info 활용해서 데이터 초기 세팅하기
        NSArray *exchangeList = [[DefaultLoader sharedInstance].exchangeInfo allKeys];
        for (NSString *exchangeName in exchangeList) {
            sharedInstance.recentPriceData[exchangeName] = [@{} mutableCopy];
        }
    });
    return sharedInstance;
}

@end

 

 

 

 

 

6. default.json 변경에 따른 exchange api 연동 파일들 수정 : Binance

 

// vim SpotBinance.h

@interface SpotBinance : 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 SpotBybit.m

#import <Foundation/Foundation.h>
#import "SpotBybit.h"
// 주요 데이터 get
#import "DefaultLoader.h"
// api 연결
#import "APIManager.h"
// 시간 unix to string
#import "CRTunixToString.h"

@implementation SpotBybit

+(instancetype)sharedInstance {
    static SpotBybit *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] default 데이터 가공 **************************************** //
    // bybit 관련 default 데이터 불러오기
    NSDictionary *bybit = [DefaultLoader sharedInstance].exchangeInfo[@"Bybit"];
    // **************************************** [Start] api 콜 준비 **************************************** //
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Bybit 현재 호가, 변동률 등 주요 데이터 가져오기 **************************************** //
    NSString *apiURL = [bybit[@"information"][@"baseUrlList"][0] stringByAppendingString:bybit[@"information"][@"publicApi"][@"ticker"]];
    apiURL = [apiURL stringByAppendingString:@"?category=spot"];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"[Error] %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            NSNumber *unixTimestamp = jsonResponse[@"time"];
            CRTunixToString *now = [CRTunixToString sharedInstance];
            [now stringFromUnix:unixTimestamp andFormat:@"yyyy-MM-dd (E) HH:mm:ss"];
            if ([jsonResponse[@"retCode"] isEqual:@0]) {
                // ****** 정상 성공시 ****** //
                // 에러가 발생하지 않은 경우 result라는 키값을 가지며 그 안에 category 및 list로 상품별 정보를 가집니다. 추가로 time, retExtInfo라는 키값을 포함해 총 4개의 데이터를 return합니다. (time은 unix timestamp입니다.)
                // result 안의 value만 추출
                NSArray* resultOfApi = jsonResponse[@"result"][@"list"];
                // api로 받은 데이터 깔끔한 dictionary로 가공하기
                for (int i=0; i<resultOfApi.count; i++) {
                    /* #################### dataFromSymbol #################### */
                    // ****** api의 데이터들 수집한거 필요시 가공해서 저장하기 ****** //
                    NSString *symbol = resultOfApi[i][@"symbol"]; // 심볼
                    NSString *price = resultOfApi[i][@"lastPrice"]; // 가격
                    // 변동률은 데이터를 다른 거래소 정보와 함께 노출시 통일성있게 하기 위해 부분 가공이 진행됩니다.
                    NSString *changePricePercent24 = [NSString stringWithFormat:@"%.2f", [resultOfApi[i][@"price24hPcnt"] floatValue] * 100]; // 근24시간 가격 변동률
                    if (![changePricePercent24 hasPrefix:@"-"]) {
                        // 음수가 아닌 경우, +로 기호 맨앞에 붙여주기
                        changePricePercent24 = [@"+" stringByAppendingString:changePricePercent24];
                    }
                    NSString *volume24 = resultOfApi[i][@"volume24h"]; // 근24시간 거래량
                    NSString *turnover24 = resultOfApi[i][@"turnover24h"]; // 근24시간 거래대금
                    // dataFromSymbol
                    if (! [self.dataFromSymbol objectForKey:symbol]) {
                        // 이미 symbol 만들어져있으면 굳이 {} 만들어서 넣어주지 않고 넘어가지만, 만약 없으면 {} 만들어서 넣어줍니다.
                        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 #################### */
                    // dataFromAsset
                    NSString *asset;
                    NSString *paymentCurrenct;
                    // 사용된 payment currency 찾고 symbol을 찢기
                    for (NSString *eachPaymentCurrenct in self.paymentCurrencyInfo[@"spot"]) {
                        // 뒤에 payment currency 매치되는거 찾은 경우 분리 진행
                        if ([symbol hasSuffix:eachPaymentCurrenct]) {
                            asset = [[symbol componentsSeparatedByString:eachPaymentCurrenct] objectAtIndex:0];
                            paymentCurrenct = eachPaymentCurrenct;
                        }
                    }
                    // dataFromAsset 데이터셋 만들어주고
                    if (asset && paymentCurrenct) {
                        // 취급하는 payment currency인 경우
                        self.dataFromAsset[asset] = [@{} mutableCopy];
                        self.dataFromAsset[asset][paymentCurrenct] = [@{} mutableCopy];
                        // dataFromAsset 데이터셋 만들어주고
                        self.dataFromAsset[asset][paymentCurrenct][@"price"] = price; // 가격
                        self.dataFromAsset[asset][paymentCurrenct][@"changePricePercent24"] = changePricePercent24; // 근24시간 가격 변동률
                        self.dataFromAsset[asset][paymentCurrenct][@"turnover24"] = turnover24; // 근24시간 거래대금
                        self.dataFromAsset[asset][paymentCurrenct][@"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(@"[WARN] Return Code : %@", jsonResponse[@"retCode"]);
                NSLog(@"[WARN] Return Message : %@", jsonResponse[@"retMsg"]);
                // api로 받은 데이터 깔끔한 dictionary로 가공하기
            }
        }
    }];
}

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

@end

 

 

 

 

 

7. default.json 변경에 따른 exchange api 연동 파일들 수정 : Bybit

 

// vim SpotBybit.h

@interface SpotBybit : 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 SpotBybit.m

#import <Foundation/Foundation.h>
#import "SpotBybit.h"
// 주요 데이터 get
#import "DefaultLoader.h"
// api 연결
#import "APIManager.h"
// 시간 unix to string
#import "CRTunixToString.h"

@implementation SpotBybit

+(instancetype)sharedInstance {
    static SpotBybit *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] default 데이터 가공 **************************************** //
    // bybit 관련 default 데이터 불러오기
    NSDictionary *bybit = [DefaultLoader sharedInstance].exchangeInfo[@"Bybit"];
    // **************************************** [Start] api 콜 준비 **************************************** //
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Bybit 현재 호가, 변동률 등 주요 데이터 가져오기 **************************************** //
    NSString *apiURL = [bybit[@"information"][@"baseUrlList"][0] stringByAppendingString:bybit[@"information"][@"publicApi"][@"ticker"]];
    apiURL = [apiURL stringByAppendingString:@"?category=spot"];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"[Error] %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            NSNumber *unixTimestamp = jsonResponse[@"time"];
            CRTunixToString *now = [CRTunixToString sharedInstance];
            [now stringFromUnix:unixTimestamp andFormat:@"yyyy-MM-dd (E) HH:mm:ss"];
            if ([jsonResponse[@"retCode"] isEqual:@0]) {
                // ****** 정상 성공시 ****** //
                // 에러가 발생하지 않은 경우 result라는 키값을 가지며 그 안에 category 및 list로 상품별 정보를 가집니다. 추가로 time, retExtInfo라는 키값을 포함해 총 4개의 데이터를 return합니다. (time은 unix timestamp입니다.)
                // result 안의 value만 추출
                NSArray* resultOfApi = jsonResponse[@"result"][@"list"];
                // api로 받은 데이터 깔끔한 dictionary로 가공하기
                for (int i=0; i<resultOfApi.count; i++) {
                    /* #################### dataFromSymbol #################### */
                    // ****** api의 데이터들 수집한거 필요시 가공해서 저장하기 ****** //
                    NSString *symbol = resultOfApi[i][@"symbol"]; // 심볼
                    NSString *price = resultOfApi[i][@"lastPrice"]; // 가격
                    // 변동률은 데이터를 다른 거래소 정보와 함께 노출시 통일성있게 하기 위해 부분 가공이 진행됩니다.
                    NSString *changePricePercent24 = [NSString stringWithFormat:@"%.2f", [resultOfApi[i][@"price24hPcnt"] floatValue] * 100]; // 근24시간 가격 변동률
                    if (![changePricePercent24 hasPrefix:@"-"]) {
                        // 음수가 아닌 경우, +로 기호 맨앞에 붙여주기
                        changePricePercent24 = [@"+" stringByAppendingString:changePricePercent24];
                    }
                    NSString *volume24 = resultOfApi[i][@"volume24h"]; // 근24시간 거래량
                    NSString *turnover24 = resultOfApi[i][@"turnover24h"]; // 근24시간 거래대금
                    // dataFromSymbol
                    if (! [self.dataFromSymbol objectForKey:symbol]) {
                        // 이미 symbol 만들어져있으면 굳이 {} 만들어서 넣어주지 않고 넘어가지만, 만약 없으면 {} 만들어서 넣어줍니다.
                        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 #################### */
                    // dataFromAsset
                    NSString *asset;
                    NSString *paymentCurrenct;
                    // 사용된 payment currency 찾고 symbol을 찢기
                    for (NSString *eachPaymentCurrenct in self.paymentCurrencyInfo[@"spot"]) {
                        // 뒤에 payment currency 매치되는거 찾은 경우 분리 진행
                        if ([symbol hasSuffix:eachPaymentCurrenct]) {
                            asset = [[symbol componentsSeparatedByString:eachPaymentCurrenct] objectAtIndex:0];
                            paymentCurrenct = eachPaymentCurrenct;
                        }
                    }
                    // dataFromAsset 데이터셋 만들어주고
                    if (asset && paymentCurrenct) {
                        // 취급하는 payment currency인 경우
                        self.dataFromAsset[asset] = [@{} mutableCopy];
                        self.dataFromAsset[asset][paymentCurrenct] = [@{} mutableCopy];
                        // dataFromAsset 데이터셋 만들어주고
                        self.dataFromAsset[asset][paymentCurrenct][@"price"] = price; // 가격
                        self.dataFromAsset[asset][paymentCurrenct][@"changePricePercent24"] = changePricePercent24; // 근24시간 가격 변동률
                        self.dataFromAsset[asset][paymentCurrenct][@"turnover24"] = turnover24; // 근24시간 거래대금
                        self.dataFromAsset[asset][paymentCurrenct][@"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(@"[WARN] Return Code : %@", jsonResponse[@"retCode"]);
                NSLog(@"[WARN] Return Message : %@", jsonResponse[@"retMsg"]);
                // api로 받은 데이터 깔끔한 dictionary로 가공하기
            }
        }
    }];
}

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

@end

 

 

 

 

 

8. default.json 변경에 따른 exchange api 연동 파일들 수정 : Bitget

 

// vim SpotBitget.h

@interface SpotBitget : 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 SpotBitget.m

#import <Foundation/Foundation.h>
#import "SpotBitget.h"
// 주요 데이터 get
#import "DefaultLoader.h"
// api 연결
#import "APIManager.h"
// 시간 unix to string
#import "CRTunixToString.h"

@implementation SpotBitget

+(instancetype)sharedInstance {
    static SpotBitget *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 *bitget = [DefaultLoader sharedInstance].exchangeInfo[@"Bitget"];
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Bybit 현재 호가, 변동률 등 주요 데이터 가져오기 **************************************** //
    NSString *apiURL = [bitget[@"information"][@"baseUrlList"][0] stringByAppendingString:bitget[@"information"][@"publicApi"][@"ticker"]];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"[Error] %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            NSNumber *unixTimestamp = jsonResponse[@"time"];
            CRTunixToString *now = [CRTunixToString sharedInstance];
            [now stringFromUnix:unixTimestamp andFormat:@"yyyy-MM-dd (E) HH:mm:ss"];
            if ([jsonResponse[@"code"] isEqual:@"00000"]) {
                // ****** 정상 성공시 ****** //
                // 기본적으로 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의 데이터들 수집한거 필요시 가공해서 저장하기 ****** //
                    NSString *symbol = resultOfApi[i][@"symbol"]; // 심볼
                    NSString *price = resultOfApi[i][@"close"]; // 가격
                    // 변동률은 데이터를 다른 거래소 정보와 함께 노출시 통일성있게 하기 위해 부분 가공이 진행됩니다.
                    NSString *changePricePercent24 = [NSString stringWithFormat:@"%.2f", [resultOfApi[i][@"change"] floatValue] * 100]; // 근24시간 가격 변동률
                    if (![changePricePercent24 hasPrefix:@"-"]) {
                        // 음수가 아닌 경우, +로 기호 맨앞에 붙여주기
                        changePricePercent24 = [@"+" stringByAppendingString:changePricePercent24];
                    }
                    NSString *volume24 = resultOfApi[i][@"quoteVol"]; // 근24시간 거래량
                    NSString *turnover24 = resultOfApi[i][@"baseVol"]; // 근24시간 거래대금
                    // dataFromSymbol
                    if (! [self.dataFromSymbol objectForKey:symbol]) {
                        // 이미 symbol 만들어져있으면 굳이 {} 만들어서 넣어주지 않고 넘어가지만, 만약 없으면 {} 만들어서 넣어줍니다.
                        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 #################### */
                    // dataFromAsset
                    NSString *asset;
                    NSString *paymentCurrenct;
                    // 사용된 payment currency 찾고 symbol을 찢기
                    for (NSString *eachPaymentCurrenct in self.paymentCurrencyInfo[@"spot"]) {
                        // 뒤에 payment currency 매치되는거 찾은 경우 분리 진행
                        if ([symbol hasSuffix:eachPaymentCurrenct]) {
                            asset = [[symbol componentsSeparatedByString:eachPaymentCurrenct] objectAtIndex:0];
                            paymentCurrenct = eachPaymentCurrenct;
                        }
                    }
                    // dataFromAsset 데이터셋 만들어주고
                    if (asset && paymentCurrenct) {
                        // 취급하는 payment currency인 경우
                        self.dataFromAsset[asset] = [@{} mutableCopy];
                        self.dataFromAsset[asset][paymentCurrenct] = [@{} mutableCopy];
                        // dataFromAsset 데이터셋 만들어주고
                        self.dataFromAsset[asset][paymentCurrenct][@"price"] = price; // 가격
                        self.dataFromAsset[asset][paymentCurrenct][@"changePricePercent24"] = changePricePercent24; // 근24시간 가격 변동률
                        self.dataFromAsset[asset][paymentCurrenct][@"turnover24"] = turnover24; // 근24시간 거래대금
                        self.dataFromAsset[asset][paymentCurrenct][@"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 *bitget = [DefaultLoader sharedInstance].exchangeInfo[@"Bitget"];
    NSArray *categoryList = @[@"spot", @"futures"];
    // 데이터 정리하기
    for (NSString *category in categoryList) {
        for (NSString *group in [bitget[category] allKeys]) {
            if ([bitget[category][group][@"category"] isEqual:category]) {
                // spot
                [_paymentCurrencyInfo[category] addObjectsFromArray:bitget[category][group][@"paymentCurrencyList"]];
            } else if ([bitget[category][group][@"category"] isEqual:@"coin-m"]) {
                // usd
                [_paymentCurrencyInfo[category] addObjectsFromArray:bitget[category][@"coinMargin"][@"paymentCurrencyList"]];
            } else if ([bitget[category][group][@"category"] isEqual:@"usd(s)-m"]) {
                // usdt
                [_paymentCurrencyInfo[category] addObjectsFromArray:bitget[category][@"stableMargin"][@"paymentCurrencyList"]];
            } else {
                NSLog(@"[WARN] Bybit spot payment currency not match : Skip...");
            }
        }
    }
}

@end

 

 

 

 

 

9. default.json 변경에 따른 exchange api 연동 파일들 수정 : Okx

 

// 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[@"information"][@"baseUrlList"][0] stringByAppendingString:okx[@"information"][@"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
                    if (! [self.dataFromSymbol objectForKey:symbol]) {
                        // 이미 symbol 만들어져있으면 굳이 {} 만들어서 넣어주지 않고 넘어가지만, 만약 없으면 {} 만들어서 넣어줍니다.
                        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

 

 

 

 

 

10. 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) 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;

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

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

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

-(void) loadPopularList;


@end

 

// vim Controller/PopluarAssetListVC.m

#import <Foundation/Foundation.h>
#import "PopluarAssetListVC.h"
// default.json 데이터 읽기
#import "DefaultLoader.h"
// 현재 시간 가져오는 용도
#import "DateTime.h"
// unix 변환용
#import "CRTunixToString.h"
// price 정보 저장
#import "AllSpotLiveData.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];
    
    // 데이터를 표시할 레이블 생성
    // **************************************** [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);
    
    if ([_exchangeInfo.allKeys containsObject:@"Binance"]) {
        /* #################### Binance 바이낸스 #################### */
        // ****** api tickers 데이터 가져오기 ****** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:2
                                         target:self
                                       selector:@selector(updateCardDataBinance)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Bybit 바이비트 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Bybit"]) {
        // ****** api tickers 데이터 가져오기 ****** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:1
                                         target:self
                                       selector:@selector(updateCardDataBybit)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Bitget 비트겟 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Bitget"]) {
        // ****** api tickers 데이터 가져오기 ****** //
        [NSTimer scheduledTimerWithTimeInterval:1
                                         target:self
                                       selector:@selector(updateCardDataBitget)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Okx 오케이엑스 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Okx"]) {
        // ****** api tickers 데이터 가져오기 ****** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:0.8
                                         target:self
                                       selector:@selector(updateCardDataOkx)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Kraken 크라켄 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Kraken"]) {
        // ** 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:1
                                     target:self
                                   selector:@selector(updateCardView)
                                   userInfo:nil
                                    repeats:YES];

}
/* #################### Binance 바이낸스 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataBinance {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotBinance *mainData = [SpotBinance sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Binance"] = mainData.dataFromSymbol;
}

/* #################### Bybit 바이비트 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataBybit {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotBybit *mainData = [SpotBybit sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Bybit"] = mainData.dataFromSymbol;
}

/* #################### Bitget 비트겟 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataBitget {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotBitget *mainData = [SpotBitget sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Bitget"] = mainData.dataFromSymbol;
}

/* #################### Okx 오케이엑스 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataOkx {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotOkx *mainData = [SpotOkx sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Okx"] = mainData.dataFromSymbol;
}

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

// **************************************** 카드뷰 다시 그리기 **************************************** //
-(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(@"****************************************************");
    }
}

// **************************************** 전체 화면 뷰 **************************************** //
/* #################### 화면 업데이트 실시 #################### */
-(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];
    
    // ****** Spot 가격 정보 불러오기 ****** //
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    
    // ****** 취급 상품 정리 및 데이터 가공 for 동일 상품에 대한 거래소간 가격 비교 기존 Array 제작 ****** //
    NSArray *exchangeNameList = [_exchangeInfo allKeys];
    
    /* #################### [Start] 카드뷰 데이터 업데이트 #################### */
    for (int i=0; i<popularList.count; i++) {
        // 기준 key인 symbol 변수 설정
        NSString *symbol = popularList[i][4];
        // ****** 카드뷰 기본 세팅 ****** //
        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<exchangeNameList.count; i++) {
            if ([allSpot.recentPriceData[exchangeNameList[i]] objectForKey:symbol]) {
                aliveSymbolCount++;
            }
        }
        if (aliveSymbolCount >= 1) {
            // 가격 정보가 1개 이상인 경우
            for (NSUInteger i=0; i<exchangeNameList.count; i++) {
                // minIndex 배정 전에, 정상적인 유효 거래소 price의 index를 일단 찾아서 minIndex로 넣고 그거랑 비교 진행
                if (minIndex != startIndex && allSpot.recentPriceData[exchangeNameList[i]][symbol][@"price"]) {
                    // minIndex랑 startIndex가 다른, 초기 상태이면서 price가 null이 아닌 유효한 값을 가질 때에만 진행, 만약 if문 안에 들어온다면 minIndex와 startIndex가 같아지면서 해당 로직 미진행
                    startIndex = i;
                    minIndex = i;
                }
            }
            for (NSUInteger i=0; i<exchangeNameList.count; i++) {
                // min, max index 찾기 실행
                if (startIndex == i) {
                    // min, max index 찾을때, startIndex는 어차피 minIndex에서 가져가면서 체크하기때문에 체크 미진행
                    // pass
                } else {
                    // min, max index 찾는 중
                    if (allSpot.recentPriceData[exchangeNameList[i]][symbol][@"price"]) {
                        // 아예 수집을 하지 못한 거래소가 최소값인 거래소로 나오지 않도록, 없는 값은 아닌지 확인! null을 floatValue 하면 0.000000 이 나오기 때문에 무조건 작은 index로 잡힙니다. null인 애는 제외하고 대소비교를 해야합니다.
                        if ([allSpot.recentPriceData[exchangeNameList[i]][symbol][@"price"] floatValue] <= [allSpot.recentPriceData[exchangeNameList[minIndex]][symbol][@"price"] floatValue]) {
                            // 가장 저렴한 Exchange index 찾기
                            minIndex = i;
                        }
                        if ([allSpot.recentPriceData[exchangeNameList[i]][symbol][@"price"] floatValue] > [allSpot.recentPriceData[exchangeNameList[maxIndex]][symbol][@"price"] floatValue]) {
                            // 가장 비싼 Exchange index 찾기
                            maxIndex = i;
                        }
                    }
                }
            }
            // 찾은 최소 index와 최대 index를 통해 top, bottom 거래소명 값 저장
            topRankPriceExchange = exchangeNameList[maxIndex];
            bottomRankPriceExchange = exchangeNameList[minIndex];
        } else {
            // 가격 정보가 모두 없을 경우 (로딩중이거나 못불러오거나) 전부 0으로 노출
            topRankPriceExchange = exchangeNameList[0];
            bottomRankPriceExchange = exchangeNameList[0];
            allSpot.recentPriceData[topRankPriceExchange][symbol][@"price"] = 0;
            allSpot.recentPriceData[bottomRankPriceExchange][symbol][@"price"] = 0;
        }
        
        // ****** top price spot 거래소명 및 이미지 설정 ****** //
        ((UIImageView *)_exchangeLogoHighImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"information"][@"image"]];
        // ****** top price spot 가격 설정 ****** //
        if (allSpot.recentPriceData[topRankPriceExchange][symbol][@"price"]) {
            // 데이터가 정상적으로 있는 경우
            // 최신 가격
            NSString *price = allSpot.recentPriceData[topRankPriceExchange][symbol][@"price"];
            ((UILabel *)_cryptoPriceHighLabelList[i]).text = price;
            
            // ****** top price spot 최근 24시간 가격 변동률 ****** //
            // 24시간 가격 변동률
            NSString *changePricePercent24 = [allSpot.recentPriceData[topRankPriceExchange][symbol][@"changePricePercent24"] stringByAppendingString:@"%"];
            ((UILabel *)_changePricePercent24HighLabelList[i]).text = changePricePercent24;
            // 24시간 가격 변동률에 색 입히거나, 없는 경우에 대한 노출 경우의 수 처리
            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 {
            // ****** top price spot 데이터가 없는 경우 ****** //
            // 비정상 상태는 아니고, 0개의 거래소에 상장되어있는 경우 데이터가 아예 없어서 이런 현상 발생 + 이 경우에는 변동률 정보도 당연히 없음
            ((UILabel *)_cryptoPriceHighLabelList[i]).text = @" - ";
            ((UILabel *)_changePricePercent24HighLabelList[i]).text = @" - ";
        }
        
        // ****** bottom price spot 거래소 이미지 설정 ****** //
        ((UIImageView *)_exchangeLogoLowImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"information"][@"image"]];
        // bottom price 가격 확보 실패시
        if (allSpot.recentPriceData[bottomRankPriceExchange][symbol][@"price"]) {
            // 데이터가 정상적으로 있는 경우
            ((UILabel *)_cryptoPriceLowLabelList[i]).text = allSpot.recentPriceData[bottomRankPriceExchange][symbol][@"price"];
            NSString *changePricePercent24 = [allSpot.recentPriceData[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 = [allSpot.recentPriceData[topRankPriceExchange][symbol][@"price"] floatValue];
        float bottomPrice = [allSpot.recentPriceData[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][@"information"][@"image"]];
        ((UIImageView *)_maxPremiumTabLowExchangeImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"information"][@"image"]];
    }
    // **************************************** [End] 카드뷰 목록 쭉 만들기 **************************************** //
}
@end

 

 

 

 

 

11. 결과 예시

 

 

 

 

 

 

+ Recent posts