2018년 12월 14일 금요일

XCTest 에서 비동기 통신 class를 테스트를 하려면?

https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations


위 링크를 보면 좋은 예제가 있다.


만일 네트워크 RESTFul API를 요청 하는 함수들 과
응답에 결과를 delegate로 호출 해주는 방법으로 만든 클래스가 있다면

이 class의 unit 테스트를 만들려면 testcase 함수 에서는 응답이 오기전에 XCAssertEqual()등의 메서드를 호출 할 수 없다. 네트워크 응답을 delegate로 받고 테스트가 진행 되어야 한다.


XCTest case 함수에서
XCTestExpectation 변수를 전역 변수로 만들고

테스트 케이스 함수에서 네트워크 요청 후
wait(for: [expectation], timeout: 10.0)
위와 같은 코드로 대기를 하게 한다.

XCAssertEqual() 메서드든 wait 코드 아래에 위치 하게 한다.

네트워크 클래스의 delegate 응답 결과를 처리 하는 protocol 메서드에서
expectation.fulfill() 을 호출 하여 대기를 풀어 준다.

그럼 wait이 풀리고 XCAssertEqual에서 테스트 판단이 이루어진다.

말로 설명한 TestCase 코드는 다음과 같다.

let expectation = XCTestExpectation(description: "Network API TEST")
    func testLogin() {
        // 1. given

        // 2. when
        network.login = Network.Login(aId: "choijh", aPassword: "1234567a")
        network.request(API: Network.API.Login, delegate: self)

        wait(for: [expectation], timeout: 10.0)

        // 3. then
        XCTAssertEqual( self.success, true, "Login success")
    }
    
    func testLoginFail() {
        // 1. given
        
        // 2. when
        network.login = Network.Login(aId: "choijh", aPassword: "a")
        network.request(API: Network.API.Login, delegate: self)
        
        wait(for: [expectation], timeout: 10.0)
        
        // 3. then
        XCTAssertEqual( self.success, false, "Login fail wrong password")

    }
    
    func onReceiveSuccessFail(_ success: Bool, api: Network.API) {
        self.success = success
        expectation.fulfill()

    }

2018년 10월 7일 일요일

Xcode에서 이미지 없이 둥근 버튼, 원 버튼 디자인 적용 하기

1. 다음과 같은 라운드 버튼을 이미지 없이





zeplin 오른쪽 사이드에 사각형 이라 표시되고 전혀 image asset에 대한 정보가 없다.
디자이너는 그냥 파란색 도형을 디자인 한것이다.

로그인 버튼은 사각형이며 파란색 도형인데 이미지가 없다?

이러한 경우는

UIVew를 상속 받은 UIButton에서 conerRadius를 설정 하여 처리 하면 된다.
결국 코딩으로 해야 한다.


2. 이번에는 속이 투명한 완전 원형 버튼






width / height 값이 동일하게 그리고 conerRadius를 width/2 로 설정 하면 원이 된다.
결국 코딩이다.

viewDidLoad에 코딩으로 하지 않고 스토리보드 인터페이스 빌더에서 속성으로 처리 하고 싶다면?


import UIKit

extension UIView {
    
    @IBInspectable var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        set {
            layer.cornerRadius = newValue
            layer.masksToBounds = newValue > 0
        }
    }
    
    @IBInspectable var borderWidth: CGFloat {
        get {
            return layer.borderWidth
        }
        set {
            layer.borderWidth = newValue
        }
    }
    
    @IBInspectable var borderColor: UIColor? {
        get {
            return UIColor(cgColor: layer.borderColor!)
        }
        set {
            layer.borderColor = newValue?.cgColor
        }
    }

}


위와 같이 extension을 작성 하면

@IBInspectable 덕에 IB에 표시된다.



extension은 @IBDesignable이 적용이 되지 않아 storyboard 랜더링은 그대로 사각형이지만 런타임에서는 훌륭한 타원으로 표출 된다.


2번 원 버튼의 경우 배경색을 투명으로 하고 border with  와 border color 를 적용 하고 corner radius를 width에 1/2로 적용 하면 원 버튼이 표현 가능 하다.





















Xcode storyboard 리팩토링

Main.storyboard에 ViewController를 계속해서 디자인 하다 보면
성능이 별로 좋지 않은 mac에서는 랜더링 속도가 현저하게 떨어진다.

ViewController에 image등을 올리는 작업이 늘어 나면 늘어 날 수록 더이상 편집 자체가
너무 느려 하기 어려워 질 수도 있다.


이렇게 하나의 Main.storyboard에 모든 ViewController를 때려 박지 않고
여러개의 storyboard 파일로 나누어 segue를 연결 할 수 있다.


스토리보드 리팩토링 방법

1. 새로운 스토리 보드 파일을 추가 한다.
2. 기존 Main.storyboard 파일과 새로 만든 스토리 보드 파일을 보기 좋게 그룹으로 생성 하여 추가 한다.
3. Main.storyboard 파일에서 일부 연결된 ViewController들을 선택 하여 Cmd + X로 자른다.
4. 새로 추가된 스토리 보드 파일에 붙여 넣기를 한다.
5. 새로 추가된 스토리 보드에 첫번째 ViewController를 is Initial View Controller로 체크 한다.
6. Main.storyboard 파일에서 새로 추가된 스토리 보드와 segue를 연결 한다.





Main.storyboard를 간단하게 바꾸고 segue에 연결은 스토리 보드 레퍼런스로 한다.


Xcode 10에서는 라이브러리 화면이 팝업으로 나오게 되어 있다. Cmd + Shift + L




두번째 항목이 스토리 보드 레퍼런스 이다.

이런한 과정을 통해 프로젝트 성격에 맡도록 storyboard 파일을 나누면
각각의 스토리 보드에서 적은수의 View Controller를 처리 하도록 하여 Xcode에서 보다 쾌적 하게 작업 할 수 있다.

2018년 9월 20일 목요일

iOS ViewController 그라데이션 배경색 적용 하기


위와 같이 zeplin에서 색상 정보가 표시되고 ViewController의 배경색을 적용 해야 하는 경우

디자이너님에게 배경 PNG를 주세요! 라고 할 수도 있고
코드로 해결 할 수도 있다.

위 두점은 그라데이션의 시작점이다.
그리고 두개의 색상 정보가 있다.

extension CAGradientLayer {
    
    convenience init(frame: CGRect, colors: [UIColor]) {
        self.init()
        self.frame = frame
        self.colors = []
        for color in colors {
            self.colors?.append(color.cgColor)
        }
        startPoint = CGPoint(x: 0, y: 0)
        endPoint = CGPoint(x: 1, y: 1)
    }
    
    convenience init(frame: CGRect, colors: [UIColor], startPoint: CGPoint, endPoint: CGPoint) {
        self.init()
        self.frame = frame
        self.colors = []
        for color in colors {
            self.colors?.append(color.cgColor)
        }
        self.startPoint = startPoint
        self.endPoint = endPoint
    }
    
    func createGradientImage() -> UIImage? {
        
        var image: UIImage? = nil
        UIGraphicsBeginImageContext(bounds.size)
        if let context = UIGraphicsGetCurrentContext() {
            render(in: context)
            image = UIGraphicsGetImageFromCurrentImageContext()
        }
        UIGraphicsEndImageContext()
        return image
    }

}

위코드에서 첫번째 convenience 초기화는 대각선 즉 0,0 ~ 1,1 로 그라데이션 을 적용 하는 것이고
추가로 두번째 convenience초기화는 startPoint와 endPoint를 파라메터로 전달 할 수 있도록 만든 초기화 이다.


위와 같이 extension을 만들고

실제 ViewController에서 imageView를 전체 영역으로 올리고 다음과 같이 코딩을 한다.

    @IBOutlet weak var backgroundImage: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        
        var colors = [UIColor]()
        colors.append( colorLiteral(red: 0, green: 0.6941176471, blue: 1, alpha: 1))
        colors.append( colorLiteral(red: 0.05882352941, green: 0.8392156863, blue: 0.8078431373, alpha: 1))
        let gradientLayer = CAGradientLayer(frame: self.view.frame, colors: colors, startPoint: CGPoint(x: 0.5, y:0), endPoint: CGPoint(x:0.5, y:1))
        backgroundImage.image = gradientLayer.createGradientImage()


    }


colorLiteral을 사용해서 숫자로 표시되지만 실제로른 Xcode에서 색상 표를 이용해 hex로 입력 한것이다.


CGPoint(x: 0.5, y:0)  ~ CGPoint(x:0.5, y:1) 은 중앙 상단에서 중앙 하단을 의미 한다.













2018년 8월 17일 금요일

네비게이션 컨트롤의 rootViewController 변경하기

 앱 최초 실행시에 intro -> 가입유도 / 또는 로그인

로그인 성공이후는 main 으로 실행 되는 앱이 있다고 하면

어느 경우는 intro가 navigation controller의 rootViewController 이고

어느 경우는 main이 navigation controller의 rootViewCotnorller  가 된다.

main으로 계속 실행되던 앱에서 logout 기능을 이용하면 login viewController가

navigationViewController에 rootViewController가 되어 실행된다.


스토리 보드를 이용하고 segue를 잘 쓰려고 하는데 앱 최초 진입점이 다르고
navigation controller를 결국에는 storyboard Embed in 으로 해서 만들 수 도 없고

결국 코드로 ViewController를 storyboard에서 찾아다가 push 하고 navigation controller를 초기 화 하고 복잡하게 ... 스파게티...


이러한 앱을 개발 할때 다음과 같이 하면 좀 수월 하다.

1. navigation controller 는 storyboard에 만들지 않는다.

2. navigation controller에 root가 될 viewController 들 ( 2개 또는 3개) 는
attribute inspector에서 top bar 항목을 설정 해준다.

그래야 하위 segue가 연결된 viewController들이 네비바가 있는것처럼 디자인 할 수 있다.

3. init viewController 및 navgation controller의 rootController를 appdelegate.swift에서 구현 한다.



didFinishLaunchingWithOptions 메서드에서 UserDefaults 등으로 구현하여 현재 상황에 맡는 ViewController를 rootViewController로 하여 navigation Controller를 설정 하고 시작 한다.

//intro
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let root = storyboard.instantiateViewController(withIdentifier: "IntroViewController")
        nvc = UINavigationController(rootViewController: root)


        window?.rootViewController = nvc

        window?.makeKeyAndVisible()


로그인 등이 완료되어 main으로 시작 하거나, 또는 로그 아웃으로 인해 다시 intro로 시작 해야 하는 경우를 대비해 
스위칭 하는 메서드를 appdelegate에 구현 한다.
func switchIntro() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "IntroViewController")
        nvc = UINavigationController(rootViewController: vc)
        
        self.window?.rootViewController = nvc
        window?.makeKeyAndVisible()
    }
    
    func switchMain() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "MainViewController")
        nvc = UINavigationController(rootViewController: vc)
        
        self.window?.rootViewController = nvc
        window?.makeKeyAndVisible()

    }



어떠한 ViewController에서 navigaion Controller의 루트를 변경 하고자 하면 다음과 같이 하면 된다.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
            
            appDelegate.switchIntro()




2018년 6월 6일 수요일

iPhone 6 7 8 plus 에서 상태바가 크게 나오는 앱 올바른 스케일로 나오게 하려면?

실제로 오래전에 만들어진 프로젝트에서 코드를 추가하다 보니

플러스 모델에서 화면 스케일이 업 되어 표출 되는 경우를 발견 했다.

실제로 아직도 내가 사용하는 iphone 6+ 에서 사용되는 앱중에 이렇게 오래된 프로그램인 듯 보이는 앱들이 존재 한다.

내가 사용중인 앱중에는  증권통,  키움증권 영웅문  과 같은 앱들이 아직도 상태바가 크게 보인다. 마치 오래전에 만든 앱이 6+에서 그냥 화면이 늘어 나서 보이는 느낌이다.

대신 증권 앱이나 기업 은행 앱은 최근에 바로 잡힌 듯 하다.



위에부터 키움증권, 증권통, facebook 의 iPhone 6 +에서 스크린샷이다.

왜 이러한 현상이 나타날까?

실제로 오래전에 만들어진 앱에 코드를 추가하고 유지보수 하다가 이러한 현상을 바로 잡기 위해 프로젝트 설정을 해메고 찾고 하였지만 해답은 의외로 간단 했다.



Launch Screen 스토리 보드르 추가 하여 위와 같이 프로젝트를 설정하면
올바른 크기로 프로그램이 실행 된다.

다음은 스택오버플로우의 관련 글이다.
https://stackoverflow.com/questions/25754942/how-to-enable-native-resolution-for-apps-on-iphone-6-and-6-plus

알면 별것 아니고 모르면 개고생이다.

증권통앱에 이방법을 알려주고 싶다.

2018년 4월 18일 수요일

Xcode 9 에서 컬러 리터럴 사용하기

코드에서 색상 값을 UIColor()에 생성자에 RGB 값으로 코딩을 하면 무슨 색인지 코드만 보고는 알수가 없다.



Xcode에서는 이러한 점을 고려하여 위와 같이 color literal을 지원하게 되었다.
색상이 Xcode에디터에서 문자 처럼 표시 된다.

color lteral을 코드에 적용 하는 방법은 다음과 같다.

color literal을 타이핑 하면 다음과 같이 코드 완성이 이루어지려 한다.


에디터에서 색상을 더블클릭 하면 색상 선택 창이 나타난다.




Other를 선택 하여 RGB 및 alpha 값을 적용 하여 원하는 색상 리터럴 문자를 만들수 있다.




이렇게 코드를 작성하면 실제는 colorLiteral() 로 코딩되어 있고 Xcode editor 상에서는 색상으로 표시되어 가독성이 좋다.











2018년 4월 12일 목요일

Xcode 9 에서 markdown파일을 렌더링 하여 보기




Xcode에서 .md 파일을 추가 할수 있다.
하지만 기본은 그냥 편집 할 수 있게만 되고 미리보기라 든지 그런 기능이 없다.


위에글은 링크된 파일을 다운로드 받으면 Xcode 프로젝트가 있는데 Xcode로 오픈해 보면 정말 README.md 가 렌더링 되서 보여준다.

아무런 차이도 없는데 해당 프로젝트는 그렇게 보여준다.
위 리크에서 설명 하듯이 답은 .xcodesamplecode.plist 파일이 프로젝트 (패키지 내용보기) 내부 에 있는 것이다. 파인더에서도 디폴트는 보여지지 않기 때문에 터미널을 이용하여 ls -al 로 확인 하였다.

sparrow-mac-mini:ARKitExample.xcodeproj sparrow$ ls -al
total 56
drwxr-xr-x@ 6 sparrow  staff    204  4 12 22:18 .
drwxr-xr-x@ 9 sparrow  staff    306  4 12 22:03 ..
-rw-r--r--@ 1 sparrow  staff    182  8  8  2017 .xcodesamplecode.plist
-rw-r--r--  1 sparrow  staff  24272  4 12 22:07 project.pbxproj
drwxr-xr-x@ 5 sparrow  staff    170  4 12 22:04 project.xcworkspace
drwxr-xr-x  3 sparrow  staff    102  4 12 22:04 xcuserdata

현재 작업중인  프로젝트는 워크스페이스로 구성되어 있었는데 여러 포함된  프로젝트 파일중에 README.md 파일을 추가 했고 렌더링 된 결과가 나올 줄 알았지만 결과는 아무런 변화가 없다.

하여 잘못된 글로 생각 했다.

워크스페이스로 된 프로젝트에서는 .xcodesamplecode.plist 파일을 workspace 디렉토리에 넣어야 된다.

결국 이글은 정확하게 잘된다.

구지 ARKitExample.xcodeproj을 다운 받지 않고도

터미널에서 워크스페이스 파일이나 xcode 프로젝트 파일에 디렉토리에 들어가서 .xcodesamplecode.plist 파일을 생성 하면 될 것으로 보인다.

해당 파일의 냉용은 다음과 같다.

sparrow-mac-mini:ARKitExample.xcodeproj sparrow$ cat .xcodesamplecode.plist 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array/>
</plist>


이파일이 프로젝트 디렉토리 (패키지내용보기) 또는 터미널에서 cd  진입 하여 .xcodesamplecode.plist (ARKitExample.xcodeproj) 에 있는 파일이나 직접 만들어 넣으면 xcode가 markdown 파일을 렌더링 해서 보여준다.

단! 렌더링 되면 수정 xcode에서는 수정이 되지 않는다.

Visual Studio Code로 편집 수정하여 Xcode 프로젝트에 추가하면 멋진 도큐먼트를 포함하는 프로젝트가 될 것이다.

그리고 표문법은 적용 되지 않는다.

그래도 이정도면 쓸만 하다.

2018년 3월 9일 금요일

UIPickerView의 선택된 텍스트 색상 변경, 선택 라인의 색상 변경을 하려면?

어쩔수 없이 UI작업이 디자인 가이드를 우선으로 해야 하는 경우 기본적인 UIPickerView를 변경 해야 하는 경우가 있다.


UIPikckerView를 디폴트로 사용하면 데이터소스로 데이터 값정도를 변경 할 수 있다.

위와 같이 선택라인의 색상을 변경 시키거나 선택된 텍스트의 컬러를 변경 시키는 작업은 코드를 이용 해야 한다.

첫번째 선택된 Text의 컬러를 변경 하기 위해서는 AttributeString을 리턴 하는 아래의 메서드를 구현 해야 한다.

func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString?
{

}

선택된 row 일때와 그렇지 않을때의 컬러를 결정 하여 NSAttributedString을 리턴 해야 한다.

1,2,3,4와 같은 UIPickerView 의 표출될 문자열 스트링 어레이가. pickerData 인 경우 
아래의 코드는 선택된 텍스트의 색상을 blue, 선택되지 않은 색상은 black으로 표출 하는 코드이다.
    func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
        var color: UIColor!
        if pickerView.selectedRow(inComponent: component) == row {
            color = UIColor.blue
        } else {
            color = UIColor.black
        }
        
        let attributes: [NSAttributedStringKey: Any] = [
            NSAttributedStringKey(rawValue: NSAttributedStringKey.foregroundColor.rawValue): color,
            NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue): UIFont.systemFont(ofSize: 15)
        ]
        
        return NSAttributedString(string: pickerData[row], attributes: attributes)
    }



다음은 선택 라인의 색상을 변경 해야 하는데 기본 UIPickerView는 Property로 제공하지 않는다.

UIPickerView를 서브클래싱 하여 CustomPickerView class를 생성 한다음 다음의 두 라인이 을 찾아서 색상을 변경 하는 코드를 추가 해야 한다.

didAddSubview 메서드를 오버라이딩 하여 서브뷰 중에 height가 1인 값의 즉 두선을 찾아 색상을 변경 한다.

 @IBDesignable와 @IBInspectable을 이용하여 스토리보드에서 변경 사항을 확인 까지 가능 하도록 다음과 같이 클래스를 생성 하면 된다.

//
//  CustomPickerView.swift
//  datetimepickerDemo
//
//  Created by  sparrow on 2018. 3. 8..
//  Copyright © 2018년  sparrow. All rights reserved.
//

import UIKit

@IBDesignable
class CustomPickerView: UIPickerView
{
    @IBInspectable var selectorColor: UIColor? = nil
    
    override func didAddSubview(_ subview: UIView) {
        super.didAddSubview(subview)
        if let color = selectorColor
        {
            if subview.bounds.height <= 1.0
            {
                subview.backgroundColor = color
            }
        }
    }
}


스토리 보드상에서 selectorColor을 설정 하면 바로 색상이 적용된다.








2018년 3월 8일 목요일

가로 모드에서 특정 뷰가 전체 화면이 되는 앱을 autolayout으로 어떻게 구현 하는가?

세로 모드에서 컨텐츠가 가운데 표시되고 상단과 하단에는 네비게이션 바 부터 정보를 보여주는 Label 하단에는 각종 버튼이 있다고 하자.

이러한 상태에서 가로모드로 전환을 하면 가운데 컨텐츠가 전체 화면으로 변경 되는 앱을 만든다고 하면 어찌 하는가?
아래 그림을 참조






화면 전환을 인지 하는 코드는. willTransition() 메서드로 인지 할 수 있다.

UIDevice.current.orientation.isLandscape 의 값을 확인 하여 현재 landscape 모드인지 확인이 가능 하다.

이럴때 가운데 컨텐츠의 autolayout constraint 값을 (0)으로 변경 하면 쉽게 해결 할 수 있다.
즉 세로모드로 IB에서 디자인을 하고 autolayout constraint를 적용 할때 Video 라는 글이 있는 검정색 뷰의 부모 뷰에 대한 constraint Align top to:와 Align Bottom to:를 IBOutlet으로 연결하여
코드로 값을 조정 하면된다.

나머지 나비게이션콘트롤러의 바나, 레이블, 버튼등도 코드로 isHidden 값을 정해 주면 된다.

하여 완성된  ViewController의 코드는 다음과 같다.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var topContraint: NSLayoutConstraint!
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!
    @IBOutlet weak var infoLabel: UILabel!
    @IBOutlet weak var controlButton: UIButton!
    
    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        if UIDevice.current.orientation.isLandscape == true {
            topContraint.constant = 0
            bottomConstraint.constant = 0
            self.navigationController?.isNavigationBarHidden = true
            infoLabel.isHidden = true
            controlButton.isHidden = true
        } else {
            topContraint.constant = 122
            bottomConstraint.constant = 300
            self.navigationController?.isNavigationBarHidden = false
            infoLabel.isHidden = false
            controlButton.isHidden = false
        }
    }


}

2018년 3월 1일 목요일

Xcode 9 GitHub 계정 지원 기능 알아 보기

xcode 9 이 되면서 Github 계정 연동 기능이 추가 되었다.
기존 버젼도 git을 지원 하기는 하였지만 좀더 많은 기능이 추가되었다.



특히 이번 버젼에는 좌측 Navigator 화면에 Source Control Navigator가 추가 되었고 직관적으로 볼수 있게 되었다.

1. github계정 생성



메뉴 Xcode -> Preferences -> Accounts
에서 Github 계정을 추가 할 수 있다.





메뉴 Xcode -> Preferences -> Source Control -> Git
에서 git 관련 설정을 할 수 있다.


2. Source Control Navigator 활용





좌측 Source Control Navigator ( Cmd + 2) 화면에서는 브랜치 전환 및 git message를 확인 할 수 있는 기능이 새롭게 추가 되었다.


3. github 계정에 원격 리포지토리 생성





Source Control Navigator에서. Remotes 마우스 우클릭으로 원격 리포지토리 생성이 가능 하다. 계정 설정에 연결된 GitHub 계정에 원격 리포지토리를 생성 할 수 있다.








branch 생성 branch 간 전환도 가능 하다.

4. 머지



test브랜치를 만들고 브랜치 전환을 하여 수정한 다음 손쉽게 마스터 브랜치에 머지 할 수 있다.


6. 소스 컨트롤 메뉴



상단 메뉴에서 Source Control 메뉴 GitHub 프로젝트에 대한 Clone , Repositores 생성 , Commit, Push, Pull 등 모든 기능을 할 수 있는 메뉴가 존재 한다.



이상으로 간단하게 Xcode 9 에 새롭게 추가된 github 연동 및 git 관련 기능을 살펴 보았다.