1. 이전 포스팅 확인하기

 

https://growingsaja.tistory.com/901

 

[Objective-C] 앱 만들기 입문 - 14 : 디자인 소폭 변경 예시 및 일정 조건 충족되는 데이터의 경우 배

1. 이전 포스팅 확인하기 https://growingsaja.tistory.com/907 [Objective-C] 앱 만들기 입문 - 13 : 특정 거래소 불필요 가격 정보(소수점 뒤에 0 나오는거) 삭제 & 출 1. 이전 포스팅 확인하기 https://growingsaja.tisto

growingsaja.tistory.com

 

 

 

 

 

2. 본 포스팅에서의 목표

 

  a. 그룹명 res를 resource로 변경

  b. 거래소의 이미지 파일명 가져오는 방식을 default.json에서 하도록 변경

  c. 싱글톤 적용해서 default.json 파일 읽어와 데이터 가져다쓰는 부분을 효율화

  d. CRTSystem 이름을 CRTDateTime로 변경

 

 

 

 

 

3. 싱글톤 패턴을 적용하기전에, 싱글톤에 대해 알기

 

 

 - 싱글톤 패턴(Singleton pattern)은 객체 지향 프로그래밍에서 특정 클래스가 인스턴스를 오직 하나만 생성하도록 보장되는 디자인 패턴입니다. 이 패턴을 사용하는 주요 이유는 다음과 같습니다.


  a. 공유 리소스 접근: 여러 객체가 동일한 리소스에 접근해야 할 때, 싱글톤을 통해 한 개의 인스턴스를 공유하여 리소스의 동시 접근을 제어할 수 있습니다. 예를 들어 프린터 스풀러나 파일 시스템에 대한 접근이 이에 해당합니다.
  b. 전역 상태 관리: 애플리케이션의 전역 상태를 관리하는 데 유용한 패턴입니다. 모든 클래스에서 액세스하는 전역 변수나 설정을 싱글톤 인스턴스에 저장할 수 있습니다. 이를 통해 다른 클래스에서도 전역 상태에 쉽게 접근할 수 있습니다.
  c. 메모리 및 성능 최적화: 어플리케이션에서 많은 메모리 또는 컴퓨팅 성능이 필요한 객체를 여러 번 생성하지 않고, 하나의 인스턴스를 생성해 재사용함으로써 메모리 및 성능 효율을 최적화할 수 있습니다.

 

 

 - 싱글톤 패턴 사용이 적합한 경우는 다음과 같습니다.


  a. 초기화 비용이 높은 객체의 생성을 제한하고 싶을 때.
  b. 공유 리소스 관리 및 중앙 집중화된 접근이 필요한 경우, 예컨대, 데이터베이스 연결이나 캐시와 같은 사례가 해당됩니다.
  c. 전역상태 관리가 필요한 경우.
     - 단, 이 패턴을 남용하면 시스템 내 명확한 경계가 없어지고, 유지 보수가 어려워질 수 있으므로 상황에 맞게 적절하게 사용해야 합니다. 또한, 멀티스레드 환경에서도 안전하게 동작하는 싱글톤 구현을 위해서는 동기화 메커니즘을 사용해야 할 수도 있습니다.

 

 

 

 

 

4. 싱글톤 적용 간단한 예제 코드

 

// vim ConfigurationManager.h

#import <Foundation/Foundation.h>

@interface ConfigurationManager : NSObject

@property (nonatomic, strong) NSDictionary *settings;

+ (instancetype)sharedInstance;

@end

 

// vim ConfigurationManager.m

#import "ConfigurationManager.h"

@implementation ConfigurationManager

+ (instancetype)sharedInstance {
    static ConfigurationManager *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[ConfigurationManager alloc] init];
        // 설정 파일을 로드하는 코드를 작성하십시오.
        sharedInstance.settings = @{@"greeting": @"hi~"};
    });
    return sharedInstance;
}

@end

 

 

 

 

 

5. 싱글톤 적용 전 default.json 파일 수정

 

{
    "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"
    }
}

 

 

 

 

 

6. 프로젝트 구조

 

scr 내의 ExchangeImage 파일들 삭제하고 default.json 파일 읽는 클래스를 DefaultLoader에 구현합니다.

 

 

 

 

 

 

 

 

 

7. 싱글톤 적용 주요 소스코드

 

// vim DefaultLoader.h

@interface DefaultLoader: NSObject

@property (readwrite, strong, nonatomic) NSArray *popularList;
@property (readwrite, strong, nonatomic) NSDictionary *exchangeImage;


+(instancetype) sharedInstance;

@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 *tryReadingDefaultJsonFile = [[CRTJSONReader alloc] init];
        NSDictionary *defaultData = [tryReadingDefaultJsonFile loadJSONFromFile:@"default"];
        sharedInstance.popularList = defaultData[@"popularList"];
        sharedInstance.exchangeImage = defaultData[@"exchangeImage"];
    });
    return sharedInstance;
}

@end

 

 

 

 

 

8. 

 

//vim CRTDateTime.h

@interface CRTDateTime : NSObject

-(NSString *)NowUTC: (NSString *)format;
-(NSString *)NowKST: (NSString *)format;

@end

 

// vim CRTDateTime.m

#import <Foundation/Foundation.h>
#import "CRTDateTime.h"

@implementation CRTDateTime

-(NSString *)NowUTC: (NSString *)format {
    // [현재 UTC 날짜 얻어 오기]
    NSDate *nowUTCDateRaw = [NSDate date];
    
    // [날짜 형식 포맷 + UTC 세팅 :: 24시간 형태]
    NSDateFormatter *dateFormatterUTC = [[NSDateFormatter alloc] init];
    [dateFormatterUTC setDateFormat:format];
    NSTimeZone *utcTimeZone = [NSTimeZone timeZoneWithName:@"UTC"];
    [dateFormatterUTC setTimeZone:utcTimeZone];
    
    // [현재 UTC 일시 출력]
    NSString *nowUTCString = @"";
    nowUTCString = [dateFormatterUTC stringFromDate:nowUTCDateRaw];
    return nowUTCString;
}

-(NSString *)NowKST: (NSString *)format {
    // [현재 UTC 날짜 얻어 오기]
    NSDate *nowUTCDateRaw = [NSDate date];
    
    // [날짜 형식 포맷 + KST 세팅 :: 24시간 형태
    NSDateFormatter *dateFormatterKST = [[NSDateFormatter alloc] init];
    [dateFormatterKST setDateFormat:format];
    NSTimeZone *kstTimeZone = [NSTimeZone timeZoneWithName:@"Asia/Seoul"];
    [dateFormatterKST setTimeZone:kstTimeZone];
    
    // [현재 KST 일시 출력]
    NSString *nowKSTString = @"";
    nowKSTString = [dateFormatterKST stringFromDate:nowUTCDateRaw];
    return nowKSTString;
}


@end

 

 

 

 

 

9. 변경된 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;



@end

 

// vim Controller/PopluarAssetListVC.m

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

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

- (void)viewDidLoad {
    [super 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];
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
    [NSTimer scheduledTimerWithTimeInterval:1
                                     target:self
                                   selector:@selector(updateDataAndView)
                                   userInfo:nil
                                    repeats:YES];
}

// 데이터 가져오기 및 뷰 업데이트 코드
- (void)updateDataAndView {
    // **************************************** [Start] api 콜 준비 **************************************** //
    CRTConnect* tryApiCall = [[CRTConnect alloc] init];
    // **************************************** [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 {
    // **************************************** [Start] 뷰 그리기 **************************************** //
    // 기존 뷰 삭제
    for (UIView *subview in self.scrollView.subviews) {
        [subview removeFromSuperview];
    }
    // viewDidLoad 메서드에서 스크롤 뷰를 초기화하고 설정합니다.
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.backgroundColor = [UIColor clearColor];
    [self.view addSubview:self.scrollView];
    
    //카드뷰 배치에 필요한 변수를 설정합니다.
    // 카드 목록 나열될 공간 세팅
    // ****************************** //
    // 목록 상단 공백 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] 최상단에 기타 정보 공간 **************************************** //
    UIView *topInfoTab = [[UIView alloc] initWithFrame:CGRectMake(cardViewXPosition, cardViewTopStartMargin - (cardViewSpacing + cardViewHeight), cardViewWidth, cardViewHeight)];
    // 국기 너비 길이 (좌우 길이) 설정
    CGFloat miniImange = 18.0;
    
    /* #################### 글로벌 지구 이모티콘 및 시간 설정 #################### */
    UILabel *earthLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard, miniImange, 20)];
    earthLabel.text = @"🌍";
    // 표준 시간 = 그리니치 표준시 : 더블린, 에든버러, 리스본, 런던, 카사블랑카, 몬로비아
    UILabel *liveUTCLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + miniImange, basicMarginInCard, cardViewWidth / 2, 20)];
    // 레이블의 텍스트를 설정합니다. 여기에서는 sortedArray 배열의 i 번째 요소를 사용합니다.
    liveUTCLabel.text = [[[CRTDateTime alloc] init] NowUTC: @"yyyy-MM-dd (E) kk:mm:ss"];
    liveUTCLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    /* #################### 한국 이모티콘 및 시간 설정 #################### */
    // 태극기
    UILabel *koreanFlagLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard + cardViewHeight / 2, miniImange, 20)];
    koreanFlagLabel.text = @"🇰🇷";
    // 한국 시간
    UILabel *liveKSTLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + miniImange, basicMarginInCard + cardViewHeight / 2, cardViewWidth / 2, 20)];
    // 레이블의 텍스트를 설정합니다. 여기에서는 sortedArray 배열의 i 번째 요소를 사용합니다.
    liveKSTLabel.text = [[[CRTDateTime alloc] init] NowKST: @"yyyy-MM-dd (E) kk:mm:ss"];
    liveKSTLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    /* #################### cardView를 self.scrollView에 추가 #################### */
    // global 적용
    [topInfoTab addSubview:earthLabel];
    [topInfoTab addSubview:liveUTCLabel];
    // korea 적용
    [topInfoTab addSubview:koreanFlagLabel];
    [topInfoTab addSubview:liveKSTLabel];
    [self.scrollView addSubview:topInfoTab];
    
    // **************************************** [Start] 카드뷰 목록 쭉 만들기 **************************************** //
    for (int i=0; i<popularList.count; i++) {
        // 기준 key인 symbol 변수 설정
        NSString *symbolKey = popularList[i][4];
        /* #################### 카드뷰 기본 세팅 #################### */
        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.text = symbolKey;
        cryptoNameLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
//        systemFontOfSize:defaultFontSize
        // 생성한 cryptoNameLabel을 cardView의 서브뷰로 추가합니다. 이렇게 함으로써 레이블이 카드 뷰에 표시됩니다.
        [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; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### 취급 상품 정리 및 데이터 가공 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;
        }
        
        /* #################### 프리미엄 비교 #################### */
        float topPrice = [popularSpotPriceList[topRankPriceExchange][symbolKey] floatValue];
        float bottomPrice = [popularSpotPriceList[bottomRankPriceExchange][symbolKey] floatValue];
        // 소수점 둘째자리 수까지
        NSString *maxPremiumPercent = [NSString stringWithFormat:@"%.2f", (topPrice-bottomPrice)/bottomPrice*100];
        
        // ****** top price spot 거래소명 및 이미지 설정 ****** //
        exchangeNameHighLabel.text = topRankPriceExchange;
        exchangeLogoHighImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[topRankPriceExchange]];
        cryptoPriceHighLabel.text = popularSpotPriceList[topRankPriceExchange][symbolKey];
        
        // ****** bottom price spot 거래소 이미지 설정 ****** //
        exchangeNameLowLabel.text = bottomRankPriceExchange;
        exchangeLogoLowImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[bottomRankPriceExchange]];
        cryptoPriceLowLabel.text = popularSpotPriceList[bottomRankPriceExchange][symbolKey];
        
        // ****** max premium spot 이미지 및 프리미엄 수치 설정 ****** //
        maxPremiumPercent = [maxPremiumPercent stringByAppendingString:@"%"];
        // 특정 값 이상 프리미엄 발생시, 텍스트에 배경색 입히기
        if ([maxPremiumPercent floatValue] >= 0.1) {
            // 0.1 이상인 경우 주황색
            NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:maxPremiumPercent];
            [attributedString addAttribute:NSBackgroundColorAttributeName
                                     value:[UIColor orangeColor]
                                     range:NSMakeRange(0, maxPremiumPercent.length)];
            // 특성 적용
            maxPricePremiumPercentLabel.attributedText = attributedString;
        } else if ([maxPremiumPercent floatValue] >= 0.05) {
            // 0.05 이상인 경우 회색
            NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:maxPremiumPercent];
            // ****** 시스템 테마 설정에 따라 색 다르게 적용 ****** //
            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)];
            }
            // 특성 적용
            maxPricePremiumPercentLabel.attributedText = attributedString;
        } else {
            maxPricePremiumPercentLabel.text = maxPremiumPercent;
        }
        
        maxPremiumTabHighExchangeImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[topRankPriceExchange]];
        maxPremiumTabLowExchangeImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeImage[bottomRankPriceExchange]];
        
        // top rank spot
        [cardView addSubview:cryptoPriceHighLabel];
        [cardView addSubview:exchangeNameHighLabel];
        [cardView addSubview:exchangeLogoHighImage];
        
        // bottom rank spot
        [cardView addSubview:cryptoPriceLowLabel];
        [cardView addSubview:exchangeNameLowLabel];
        [cardView addSubview:exchangeLogoLowImage];
        
        // spot max premium % in spot
        [cardView addSubview:maxPricePremiumPercentLabel];
        [cardView addSubview:maxPremiumTabHighExchangeImage];
        [cardView addSubview:maxPremiumTabLowExchangeImage];
        
        // cardView를 self.scrollView에 추가합니다.
        [self.scrollView addSubview:cardView];
    }
    // **************************************** [End] 카드뷰 목록 쭉 만들기 **************************************** //
    // 상하 스크롤 최대치 자동 설정
    CGFloat contentHeight = popularList.count * (cardViewHeight + cardViewSpacing);
    self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, contentHeight);
}
@end

 

 

 

 

 

10. 결과 예시

 

보이는 상으로 달라진 점은 없습니다만 코드가 더 깔끔해졌습니다.

 

 

 

 

 

 

 

+ Recent posts