^(코딩캣)^ = @"코딩"하는 고양이;
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(5) - closure
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 15. 19:44

[번역글] View Controller 사이에 데이터를 교환하는 방법(5) - closure

Language/Objective-C & Swift
2021. 9. 15. 19:44

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법

클로저(closure)를 사용하여 뷰 컨트롤러 사이에 데이터를 전달하는 것은 프로퍼티나 델리게이션을 사용하는 것과 크게 다르지 않습니다. 클로저를 사용하면 상대적으로 사용하기가 쉽고 또한 별도의 메소드(함수)나 프로토콜을 선언하지 않아도 지역적으로 데이터 교환 구문을 정의할 수 있다는 이점이 있습니다.

클로저를 사용하여 뷰 컨트롤러 사이에 데이터를 주고받을 수 있다.

나중에 등장하게 될 뷰 컨트롤러에 다음과 같이 프로퍼티를 하나 만드는 것으로 소스 코드를 시작합니다.

// Swift
var completionHandler: ((String) -> Int)?
// Objective-C
@interface ...
@property (atomic, weak) NSInteger (^completionHandler)(NSString *);
@end
@implementation ...
@synthesize completionHandler;
@end

 

여기서 completionHandler는 클로저 타입을 가지고 있습니다. 이 클로저는 옵셔널(optional)이기 때문에 끝에 ?가 붙습니다. 또한 클로저의 시그니처(signature)는 (String) -> Int입니다. 이것은 이 클로저가 String형 매개변수를 하나 받고 Int형 값을 반환하는 메소드(함수 또는 람다식)임을 뜻합니다.

나중에 등장할 뷰 컨트롤러에서 작성할 또 하나의 사항은 버튼이 탭(tap)되었을 때 그 클로저를 호출하는 것입니다.

// Swift
@IBAction func onButtonTap(sender: Any) {
    let result = completionHandler?("FooBar")
    print("completionHandler returns ... \(result)")
}
// Objective-C
-(IBAction) onButtonTapWithSender:(id)sender {
    NSInteger result;
    
    if ([self completionHandler] != nil) {
        result = [self completionHandler](@"FooBar");
        NSLog("completionHandler returns ... %d", result);
    }
}

 

제시된 예제는 다음과 같이 작동합니다.

한 개의 매개변수가 지정되면서 completionHandler 클로저가 호출되면, 그 클로저의 호출 결과가 result 변수에 보관됩니다.

결과는 print()의 호출에 의해 출력됩니다.

그 다음 MainViewController에서 여러분은 다음과 같이 클로저를 정의합니다.

// Swift
secondaryViewController.completionHandler = { text in
    print("text = \"\(text)\"")
    return text.characters.count
}
// Objective-C
[[self secondaryViewController] setCompletionHandler: ^(NSString * text) {
    NSLog("text = \"%@\", text);
    return (NSInteger)[text length];
}

 

이것이 클로저 그 자체입니다. 이것은 지역 범위에서 선언되었기 때문에 클로저가 선언된 스코프 내의 로컬 변수, 프로퍼티 및 메소드(함수)들을 모두 사용할 수 있습니다.

text 매개변수가 출력되어 나오는 클로저에서는 실행의 결과로서 문자열의 길이가 반환됩니다. 여기서 흥미로운 사실이 발견됩니다. 이런 식으로 작성되는 클로저는 뷰 컨트롤러 사이에서 데이터를 양방향으로 전달해준다는 것입니다. 여러분이 클로저를 정의하고 유입될 데이터에 대하여 작업한 다음 이 클로저를 호출한 코드에게 결과 데이터를 반환할 수 있습니다.

물론 여러분은 델리게이션을 사용하거나 프로퍼티 자체를 사용한 메소드(함수) 호출에서도 그 코드 블록을 호출해 준 코드에게 소정의 값을 반환할 수 있다는 것을 알고 있을 지 모릅니다. 전적으로 맞습니다. 다음과 같은 상황에서 클로저는 여러분의 손에 잡히게 될 것입니다.

(1) 여러분은 단지 짧고 간략한 내용의 함수를 작성하고자 굳이 프로토콜을 만들어가며 완전한 델리게이션 방식으로 접근하기를 원하지 않을 수 있습니다.

(2) 여러분은 여러 클래스에 걸쳐 하나의 클로저를 전달하고 싶어질 수도 있습니다. 클로저가 없었다면 여러분은 쏟아지는 함수 호출들을 만들었어야 하겠으나, 클로저와 함께 여러분은 한 블록의 코드만 전달하면 됩니다.

(3) 어떤 데이터가 지역적으로만 존재할 때 여러분은 클로저 안에서 그 데이터에 접근할 수 있는 수준에서 코드 한 블록을 지역적으로(locally) 정의할 필요가 있을 것입니다.

클로저를 사용하여 뷰 컨트롤러 사이에 데이터 전달이 이뤄지게 할 때의 손해는, 여러분의 코드가 다소 복잡해 보일 수 있다는 것입니다. 다른 방법을 사용할 때보다 클로저를 쓰는 것이 더 이치에 맞다고 여겨질 때, 뷰 컨트롤러 사이에 데이터를 전달할 때에 한하여 클로저를 사용하는 것이 현명한 선택입니다.

그렇다면 두 개의 뷰 컨트롤러 사이에 연결고리가 형성되지 않았거나 형성될 수 없음에도 데이터를 주고 받기 위해서는 어떻게 해야 할까요? 다음 장에서 살펴보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(4) - delegation
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 14. 23:05

[번역글] View Controller 사이에 데이터를 교환하는 방법(4) - delegation

Language/Objective-C & Swift
2021. 9. 14. 23:05

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

델리게이션(delegation)을 사용하여 데이터를 되돌려 보내기

델리게이션(delegation)은 iOS SDK에서 중요하면서 빈번하게 사용되는 소프트웨어 디자인 패턴입니다. 여러분이 iOS 앱을 코딩할 것이라면 이를 이해하는 것이 필수적입니다!

델리게이션을 사용하면 부모 클래스(base class)는 파생 클래스(secondary class)의 기능에 대해 손을 떼어도 됩니다. 코더는 파생 클래스를 구현하면서 프로토콜(protocol)을 사용하기에 부모 클래스로부터 전해지는 이벤트에 반응만 하면 됩니다. 즉 부모 클래스와 파생 클래스 사이에 결합성 및 의존성이 줄어든다는 것입니다.

간단한 예를 통해 이해해 보겠습니다.

(1) 여러분이 피자 레스토랑에서 일하며 피자를 굽고 있다고 상상해 봅니다.

(2) 고객은 피자를 가지고 여러 가지를 할 수 있습니다. 바로 먹을 수도 있고, 나중에 먹으려고 얼려 놓을 수도 있고, 친구와 나눠먹을 수도 있습니다.

(3) 피자를 굽는 입장에서는 피자를 만드는 작업에 대해서만 생각하면 되고, 피자를 어떻게 먹을 것인지에 대한 작업과는 분리(decoupled)되어 있습니다. 단지 여러분은 피자가 완성되었을 때 고객에게 이를 가져갈 수 있게 알려주기만 하면 됩니다. 다시 말하면 피자를 먹는 방법에 대해서는 위임(delegate)해 버리고 여러분은 피자 굽는 일 자체에만 집중하면 됩니다.

델리게이션을 사용하여 뷰 컨트롤러 사이에 데이터를 교환한다.

피자를 굽는 여러분과 피자를 받아 먹게 될 고객이 서로를 이해하기 위하여 여러분은 “프로토콜(protocol)”을 정의할 필요가 있습니다.

// Swift
protocol PizzaDelegate {
    func onPizzaReady(type: String)
}
// Objective-C
@protocol PizzaDelegate {
    -(void) onPizzaReadyWithType:(NSString *)type;
}>

 

“프로토콜(protocol)”은 어떤 클래스가 구현해야 하는 함수(메소드)들에 대한 규약입니다. 클래스가 프로토콜을 준수하도록 하고 싶다면 다음과 같은 구문을 클래스 선언할 때 적어주면 됩니다.

// Swift
class MainViewController: UIViewController, PizzaDelegate {
    // ...
}
// Objective-C
@interface MainViewController: UIViewController<PizzaDelegate>
    // ...
@end

 

위와 같은 구문은,

(1) 클래스의 이름이 MainViewController이고;

(2) UIViewController에서 확장된 서브클래스이면서;

(3) PizzaDelegate 프로토콜을 준수(구현)하는 클래스라는 의미를 가지고 있습니다.

여러분이 이 클래스에 대해 프로토콜을 구현한다고 적었다면 이를 실제로 구현해주어야 합니다. 다음과 같이 MainViewController에 메소드(함수)를 적어줍니다.

// Swift
func onPizzaReady(type: String) {
    print("Pizza ready. type = \"\(type)\"")
}
// Objective-C
-(void) onPizzaReadyWithType:(NSString *)type {
    NSLog(@"Pizza ready. type = \"%@\"", type)
}

 

여러분이 나중에 등장할 뷰 컨트롤러에 대한 클래스 인스턴스를 생성할 때, 앞서 살펴본 예제에서와 같이 델리게이트를 연결시켜줍니다.

// Swift
let secondaryViewController = SecondaryViewController()
secondaryViewController.delegate = self
// Objective-C
SecondaryViewController * secondaryViewController
    = [[SecondaryViewController alloc] init];
[secondaryViewController setDelegate: self];

 

그리고나서 델리게이션의 진면목이 나타나게 됩니다. 나중에 등장하는 뷰 컨트롤러를 작성할 때 여러분은 프로퍼티를 추가하고 몇 가지 코드를 적습니다. 먼저 프로퍼티는 이렇게 적습니다.

// Swift
weak var delegate: PizzaDelegate?
// Objective-C
@interface ...
    @property (atomic, weak) PizzaDelegate * delegate;
@end
@implement ...
    @synthesize delegate;
@end

 

그리고 다음과 같이 메소드(함수)를 작성합니다.

// Swift
@IBAction func onButtonTap() {
    delegate?.onPizzaReady(type: "Pizza di Mama")
}
// Objective-C
-(IBAction) onButtonTap {
    [[self delegate] onPizzaReadyWithType: @"Pizza di Mama"];
}

 

onButtonTap()이 피자가 완성되었을 때 호출된다고 가정하면 이 메소드(함수)는 델리게이트에서 선언된 onPizzaReady(type:)을 호출합니다.

피자를 굽는 입장에서는 delegate 프로퍼티로 지정된 객체가 있는지 아니면 nil인지 여부에는 관심이 없습니다. 그저 피자를 완성시켜서 고객을 향해 내어 놓기만 하면 됩니다. 이로써 여러분은 고객이 피자를 어떤 식으로 먹든 상관하지 않을 수 있게 됩니다.

 

번역자 추가 설명 만일 Swift로 weak var 델리게이트를 작성 시 “'weak' must not be applied to non-class-bound...”로 시작하는 오류가 발생한다면, 프로토콜을 선언할 때 무조건 참조 형식인 클래스에서만 사용되도록 제한함으로써 해소시킬 수 있다.

// Swift
protocol PizzaDelegate: class {
    // ...
}

그러면 class 키워드를 써서 제한하는 것은 deprecated되었다는 경고가 뜰 수 있다. 이 때는 다음과 같이 수정한다.

// Swift
protocol PizzaDelegate: AnyObject {
    // ...
}

 

델리게이션을 통해 뷰 컨트롤러 사이에 데이터를 주고 받는 핵심적인 과정을 짚어보면 다음과 같습니다.

(1) 델리게이트 프로토콜 선언이 필요합니다.

(2) 델리게이트 프로퍼티 선언이 필요합니다.

(3) 여러분이 데이터 처리에 관여하고 싶지 않은 클래스가 있다면, 데이터를 받을 그 클래스는 해당 프로토콜을 구현하고 있어야 합니다.

(4) 여러분이 데이터를 전달한 후의 과정에 관여하고 싶지 않다면 프로토콜에서 선언된 메소드(함수)를 호출함으로써 데이터를 전달하면 됩니다.

 

앞서 살펴보았던 프로퍼티만을 사용한 예제하고 여기서 다루어 본 델리게이션을 사용한 예자하고 비교해 보았을때 어떤 차이점이 있을까요? 다음과 같이 정리할 수 있습니다.

(1) 각각의 클래스 개발을 맡은 서로 다른 개발자들은 프로토콜 및 그 프로토콜이 갖는 메소드(함수)에 대해서만 합의를 보면 됩니다. 각 개발자들은 자신의 클래스가 프로토콜을 준수할 것인지 아닌지를 선택할 수 있고, 필요하다면 프로토콜 내 메소드들을 구현할 수 있습니다.

(2) MainViewControllerSecondaryViewController 사이에는 직접적인 참조가 없습니다. 이는 앞선 예제에 비해 뷰 컨트롤러 객체들끼리 다소 느슨하게 결합되었음을 의미합니다.

(3) 여기서 선언한 프로토콜은 MainViewController 뿐만 아니라 어떤 클래스에서든 구현될 수 있습니다.

 

굉장합니다. 이제 클로저(closure)를 사용하는 또 다른 예제에 대해 살펴보도록 하겠습니다.

참고 weak라고 표시된 델리게이트 프로퍼티는 무엇일까요? Automatic Reference Counting(ARC) in Swift를 참고해 보세요.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(3) - property와 method
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 14. 21:12

[번역글] View Controller 사이에 데이터를 교환하는 방법(3) - property와 method

Language/Objective-C & Swift
2021. 9. 14. 21:12

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

프로퍼티(property)와 메소드(method, function)를 사용하는 방법(A ← B 방향)

나중에 등장한 뷰 컨트롤러가 자신을 호출해 준 뷰 컨트롤러에게 어떤 값을 돌려 보내려면 어떻게 해야 할까요?

프로퍼티를 사용하여 나중에 등장할 뷰 컨트롤러로 데이터를 전달하는 방법은 이미 살펴보았지만 확실히 순방향(straightforward)으로의 전달입니다. 그렇다면 거꾸로 자신을 호출해 준 뷰 컨트롤러로 어떤 값을 전달해 주려면 어떻게 해야 할까요?

이러한 상황이 필요한 시나리오입니다.

1) 사용자가 뷰 컨트롤러 A를 떠나서 뷰 컨트롤러 B로 향합니다.

2) 나중에 나타난 뷰 컨트롤러에서 사용자는 데이터를 가지고 상호작용하고 있고 여러분은 그 데이터를 뷰 컨트롤러 A로 되돌려 보내고 싶습니다.

다시 말하자면 A에서 B로 데이터를 전달하는 것이 아니라 B에서 A로 데이터를 전달하는 것입니다.

...

데이터를 돌려보내는 가장 쉬운 방법은 뷰 컨트롤러 B에다가 뷰 컨트롤러 A에 대한 참조를 생성하는 것이고, 뷰 컨트롤러 B에서 그 A의 함수(메소드)를 호출하는 것입니다. 그렇다면 나중에 나타나는 뷰 컨트롤러 B의 소스 코드는 다음과 같을 것입니다.

// Swift
class SecondaryViewController: UIViewController {
    var mainViewController: MainViewController!
    
    @IBAction func onButtonTap() {
        mainViewController.onUserAction(data: "Hello, World!")
    }
}
// Objective-C
@interface SecondaryViewController: UIViewController {
    @property (atomic, retain) MainViewController * mainViewController;
}
@implement SecondaryViewController {
    @synthesize mainViewController;
    
    -(IBAction) onButtonTap {
        [[self mainViewController] onUserActionWithData: @"Hello, World!"];
    }
}

 

그리고 MainViewController는 다음과 같은 메소드(함수)가 추가될 것입니다.

// Swift
func onUserAction(data: String) {
    print("Data received: \"\(data)\"")
    // ...
}
// Objective-C
-(void) onUserActionWithData:(NSString *)data {
    NSLog(@"Data received: \"%@\"", data);
    // ...
}

 

그리고 이전 장에서 앞서 보였던 예제와 같이 나중에 등장할 뷰 컨트롤러가 생성된 후 내비게이션 스택에 푸쉬(push)될 것입니다. 이 때 MainViewControllerSecondaryViewController 사이의 연결은 다음과 같이 만들어집니다.

// Swift
class MainViewController: UIViewController {
    // ...
    let secondaryViewController = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil)
    secondaryViewController.mainViewController = self
// Objective-C
@implement MainViewController {
    // ...
    SecondaryViewController * secondaryViewController
        = [[SecondaryViewController alloc] init];
    [secondaryViewController setMainViewController: self];
}

 

위의 예제에서 보듯 mainViewController라는 프로퍼티에 self가 대입됩니다. 이로써 나중에 등장할 뷰 컨트롤러는 자신을 호출해 준 뷰 컨트롤러의 존재를 알고 있습니다. 때문에 나중에 등장할 뷰 컨트롤러는 onUserAction(data:)와 같이 해당 뷰 컨트롤러의 메소드(함수)들을 호출할 수 있습니다.

이것은 전부입니다. 하지만... 데이터를 전달하기 위한 이와 같은 접근법은 그리 이상적인 방안이 아닙니다. 몇 가지 문제점을 꼽자면 다음과 같습니다.

(1) MainViewControllerSecondaryViewController는 너무 강하게 결합되어 있습니다. 여러분은 소프트웨어를 설계할 때 강한 결합성을 피하라는 말을 들어 보았을 것이고 그렇게 하기를 원할 것입니다. 왜냐하면 강한 결합성은 여러분의 코드에 대한 모듈화 정도를 떨어뜨리기 때문입니다. 이와 같이 작성된 두 개의 클래스는 너무 과도하게 얽혀있으면서 서로의 메소드(함수)에 의존하고 있는데 이는 종종 “스파게티 코드”로 이어지게 됩니다.

(2) 위의 예제 코드에서는 객체에 대한 retain이 순환합니다. 나중에 등장한 뷰 컨트롤러 객체는 먼저 등장했던 뷰 컨트롤러가 제거될 때까지는 메모리에서 해제되지 않습니다. 하지만 먼저 등장했던 뷰 컨트롤러도 나중에 등장한 뷰 컨트롤러가 제거되기 전까지는 메모리에서 해제될 수 없습니다. 이에 대한 해법은 weak 프로퍼티 키워드를 사용하는 것입니다.

(3) 만일 MainViewControllerSecondaryViewController의 개발자가 서로 다르다면 두 클래스에 대한 작업이 쉽게 수행될 수 없습니다. 왜냐하면 두 개의 뷰 컨트롤러는 서로의 뷰 컨트롤러가 어떻게 작동되는지를 이해해야 할 필요가 있기 때문입니다. 이 경우 두 개발자 사이에는 관심사의 분리(separation of concerns)가 이뤄질 수 없습니다.

이제 여러분은 클래스, 인스턴스 및 메소드(함수)들을 위와 같이 직접적으로 참조하는 것을 피하고 싶어질 것입니다. 이와 같이 코딩해 둔다면 나중에 유지관리하는 데 악몽이 될 수도 있습니다. 이러한 방식은 종종 스파게티 코드로 이어지면서 여러분이 코드의 어느 일부분을 수정할 경우 이와 관련된 다른 부분들도 연쇄적으로 충돌이 발생하게 됩니다. 그렇다면 어떻게 하는 것이 좋을까요? 그건 델리게이션(delegation)을 사용하는 것입니다.

간단한 요령 뷰 컨트롤러 사이에 소속될 변수가 여러 개 있다면, 프로퍼티를 이에 맞춰 여러 개 만들려 하지 마세요. 그 대신 필요한 모든 데이터들을 담는 구조체(struct) 또는 클래스(class)를 선언 및 정의하고 이 형식으로 표현되는 하나의 프로퍼티를 만들어서 뷰 컨트롤러 사이에 전달을 수행하면 됩니다.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(2) - segue
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 14. 11:33

[번역글] View Controller 사이에 데이터를 교환하는 방법(2) - segue

Language/Objective-C & Swift
2021. 9. 14. 11:33

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법(A → B 방향)

스토리보드를 사용하고 있다면 여러분은 세그웨(segue)prepare(for:sender:) 메소드를 사용하여 뷰 컨트롤러 사이에 데이터를 전달할 수 있습니다.

스토리보드와 세그웨를 사용하여 뷰 컨트롤러 사이에 데이터를 전달하는 것은 앞서 XIB를 사용한 방법과 크게 다르지 않습니다. 스토리보드와 세그웨의 의미를 간단히 짚고 넘어가자면, 스토리보드는 여러분의 앱이 갖는 사용자 인터페이스들의 집합체입니다. Xcode의 Interface Builder를 가지고 생성할 수 있고 코드 작성을 최소화면서 뷰 컨트롤러 사이의 전환을 만들 수 있습니다.

세그웨(segue)는 부드러운 화면 전환(smooth transition)을 뜻합니다. 여러분이 예를 들어 내비게이션 컨트롤러를 가지고 어떤 뷰 컨트롤러에서 다른 뷰 컨트롤러로 전환시킬 때, 여러분은 이미 세그웨를 만든 것입니다. 여러분의 뷰 컨트롤러에서 이 세그웨에 후킹(hooking)하여 그 작동을 수정할 수도 있습니다. 뷰 컨트롤러 사이에서 발생하는 데이터 교환도 세그웨 과정 도중에 발생합니다.

이번 예제에서 여러분은 MainViewController에서 TertiaryViewController로 전환되는 세그웨를 보게 될 것입니다. 이를 위해 어떤 액션을 한 버튼에서 다른 뷰 컨트롤러로 이어준 다음 "Show" 유형을 선택합니다. 그러면 MainViewController에서 TertiaryViewController로 이어지는 화살표가 나타날 것입니다. 그 다음 뷰 컨트롤러의 "Identity Inspector"에서 커스텀 클래스를 TertiaryViewController로 지정합니다.

TertiaryViewController의 코드는 다음과 같습니다.

// Swift
class TertiaryViewController: UIViewController {
    var username: String = ""
    
    @IBOutlet weak var usernameLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        usernameLabel.text = username
    }
}
// Objective-C
@interface TertiaryViewController: UIViewController {
    @property (atomic, strong) NSString * username;
    @property (nonatomic, weak) IBOutlet UILabel * usernameLabel;
@end
@implement TertiaryViewController
    @synthesize username;
    @synthesize usernameLabel;
    
    -(void) viewDidLoad {
        [super viewDidLoad];
        [[self usernameLabel] setText: [self username]];
    }
}

 

특별할 것도 없이 이 예제는 앞서 살펴보았던 것과 크게 다르지 않습니다. 여러분은 이 예제를 통해 usernameLabel이라는 라벨의 텍스트를 username이라는 프로퍼티가 현재 갖고 있는 문자열로 설정한 것일 뿐입니다.

이제 MainViewController에서 TertiaryViewController로 데이터를 전달해 보겠습니다. 이 때 prepare(for:sender:)라 불리는 특별한 메소드를 사용해야 하는데 이 메소드는 세그웨를 하기 전에 호출되는 메소드이므로 여러분은 이를 사용자화할 수 있습니다. 세그웨 직전 수행하게 될 코드는 다음과 같습니다.

// Swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if (segue.destination is TertiaryViewController) {
        let tertiaryViewController = segue.destination as? TertiaryViewController
        tertiaryViewController.username = "Arthur Dent"
    }
}
// Objective-C
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([[segue destination] isKindOfClass: [TertiaryViewController class]]) {
        TertiaryViewController * tertiaryViewController = (TertiaryViewController *)[segue destination];
        [tertiaryViewController setUsername: @"Arthur Dent"];
    }
}

 

위 코드에서 어떤 작업들이 수행되는지 살펴보겠습니다.

먼저 if문과 is 키워드는 세그웨의 목적지가 TertiaryViewController 클래스형인지를 검사하게 해 줍니다. 모든 세그웨는 이 prepare(for:sender:) 메소드를 거쳐가기 때문에 이 세그웨가 여러분이 사용자화하고 싶은 그 세그웨인지 아닌지를 검사할 필요가 있습니다.

그 다음 username 프로퍼티를 사용하기 위하여 segue.destinationTertiaryViewController 클래스형으로 형변환합니다. segue 매개변수를 통해 전달되는 destination 프로퍼티는 UIViewController 형식으로 되어 있기 때문에 TertiaryViewController에서 선언한 username 프로퍼티를 사용하려면 이처럼 다운캐스트해야 합니다.

마지막으로 username 프로퍼티를 설정합니다. 이는 앞서 살펴 본 예제와 동일합니다.

prepare(for:sender:) 메소드에 대한 재미있는 사실은 여러분이 이 메소드에서 특정 세그웨 이외의 다른 것에 대해서는 작업할 필요가 없다는 것입니다. 이 메소드는 단순히 세그웨에 훅(hook)되기 때문에 화면 전환을 위해 이어서 나머지 작업을 수행하도록 어떤 코드들을 작성할 필요가 없다는 것입니다. 여러분은 여러분이 새로 구성한 뷰 컨트롤러를 반환할 필요도 없습니다.

또한 여러분은 위 Swift 코드를 다음과 같이 축약할 수도 있습니다.

// Swift
if let tertiaryViewController = segue.destination as? TertiaryViewController {
    tertiaryViewController.username = "Ford Perfect"
}

 

destination의 타입을 검사하기 위해 is를 사용하고 캐스트하는 대신 옵셔널 캐스팅(optional casting)을 사용하여 한 문장으로 처리할 수도 있습니다. segue.destinationTertiaryViewController 타입이 아니라면 as? 식은 nil을 반환할 것이고 조건문에 딸린 문장은 실행되지 않을 것입니다.

만일 형변환(type casting)을 사용하고 싶지 않다면 segue.identifier 프로퍼티로 특정 세그웨를 식별할 수도 있습니다. 예를 들어 스토리보드에서 이 프로퍼티에 "tertiaryVC"라는 문자열을 부여한 다음 다음과 같이 코드를 작성할 수도 있습니다.

// Swift
if segue.identifier == "tertiaryVC" {
    // 수행할 작업
}
// Objective-C
if ([[segue identifier] isEqualToString:@"tertiaryVC"]) {
    // 수행할 작업
}

 

세그웨와 스토리보드를 사용하여 두 개의 뷰 컨트롤러 사이에 데이터를 전달하는 방법은 여기까지입니다.

대다수의 앱에서 스토리보드는 여러분이 사용할 수 있는 뷰 컨트롤러 사이의 전환을 제한합니다. 스토리보드는 종종 Xcode에서 사용자 인터페이스 만드는 것을 과도하게 복잡하게 만들기도 하는데 이에 비해 얻는 이점은 다소 적습니다. 무엇보다도 여러분이 복잡한 구조로 스토리보드나 XIB를 작성한다면 Interface Builder는 느려지고 버벅거립니다.

스토리보드로 작성할 수 있는 모든 것은 사실 여러분이 직접 손으로 코딩하여 구현할 수도 있습니다. 이 때는 개발자가 약간의 노력만 한다면 보다 상세한 제어가 가능합니다. 그렇다고 해서 필자는 여러분에게 손으로 일일이 다 코딩하라고 말하는 것은 아닙니다! 위의 예제처럼 하나의 뷰 컨트롤러당 하나의 XIB를 작성하고, UITableViewCell과 같은 서브 클래스 뷰(sub class view) 하나당 하나의 XIB를 작성합니다.

코더로서 궁극적으로 여러분은 탭 문자로 들여쓸 것인지 스페이스로 들여쓸 것인지, 스토리보드를 사용할 것인지 XIB를 사용할 것인지 아니면 Core Data를 쓸 것인지 Realm을 쓸 것인지 등등의 갈림길에서 여러분만의 최선의 선택을 찾고 싶어질 것입니다. 그것은 전적으로 여러분에게 달려 있습니다.

흥미로운 사실 모든 개발자들은 “segue”라는 단어를 자신만의 방법대로 발음하고 있습니다. 몇몇은 “se-” 부분을 “say”나 “set”의 “-e-”처럼 “-에-”로 발음하고, “-gue” 부분을 “gue_rilla”처럼 “-그웨”와 같이 발음합니다. 다른 이들은 “segway”처럼 “세그웨이”로 발음하기도 합니다. (segway: 여행자들을 위한 개인형 이동수단의 일종으로서 한 쌍의 바퀴로만 되어 있고 스스로 균형을 잡는 기구)

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(1) - property
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 뷰 컨트롤러(View Controller) 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App archite..
Language/Objective-C & Swift
2021. 9. 13. 20:51

[번역글] View Controller 사이에 데이터를 교환하는 방법(1) - property

Language/Objective-C & Swift
2021. 9. 13. 20:51

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 뷰 컨트롤러(View Controller) 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

프로퍼티를 사용하여 뷰 컨트롤러 사이에 데이터를 주고 받기(A → B 방향)

이미 언급했듯이 일부 접근법들은 데이터가 한 뷰 컨트롤러에서 다른 뷰 컨트롤러로 전달되는 단방향성을 갖습니다. 말하자면 그런 일부의 방법은 대칭(bilateral)적이지 않다는 것입니다. 하지만 염려할 것은 없습니다. 대부분의 경우 여러분은 단방향 통신을 필요로 할 것이기 때문입니다.

뷰 컨트롤러 A(현재의 화면)에서 뷰 컨트롤러 B(다음으로 넘어갈 화면)로 데이터를 넘기는 가장 쉬운 방법은 프로퍼티(property)를 사용하는 방법입니다. 프로퍼티는 그 클래스의 일부인 변수입니다. 그러한 클래스로부터 만들어진 모든 인스턴스는 따로따로 프로퍼티를 가지고 있고 여러분은 여기에 값을 배정할 수 있습니다. 다른 클래스와 마찬가지로 UIViewController의 파생형인 뷰 컨트롤러들은 프로퍼티를 가질 수 있습니다.

프로퍼티를 사용하여 뷰 컨트롤러 사이에 데이터를 전달하기.

다음과 같이 text라는 이름을 가진 프로퍼티가 있는 뷰 컨트롤러인 MainViewController가 있습니다.

// Swift
class MainViewController: UIViewController {
    var text: String = ""
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
// Objective-C
@interface MainViewController: UIViewController {
    @property (atomic, strong) NSString * text;
}

@implementation MainViewController {
    @synthesize text
    
    -(void) viewDidLoad {
        [super viewDidLoad];
    }
}

 

여러분이 MainViewController 클래스 인스턴스를 만들 때마다, 여러분은 다음과 같이 text 프로퍼티에 값을 배정할 수 있습니다.

// Swift
let mainViewController = MainViewController()
mainViewController.text = "Lorem Ipsum"
// Objective-C
MainViewController * mainViewController = [[MainViewController alloc] init];
[mainViewController setText: @"Lorem Ipsum"];

 

참 쉽죠? 이제 이 뷰 컨트롤러가 내비게이션 컨트롤러(Navigation Controller)의 일부라고 가정하고, 이 뷰 컨트롤러에서 다음에 등장할 뷰 컨트롤러로 어떤 값을 전달할 것입니다.

먼저 여러분이 스토리보드(Storyboard)를 사용하고 있다면 이 MainViewController에다가 내비게이션 컨트롤러를 삽입하고 싶을 것입니다. 스토리보드를 사용하고 있지 않다면 여러분은 내비게이션 컨트롤러를 새로 생성한 다음 MainViewController를 그것의 root view controller로 지정할 수 있습니다. 이미 내비게이션 컨트롤러가 적용되어 있다면 완벽합니다!

그 다음 여러분은 새로운 뷰 컨트롤러를 생성하기 위하여 .xib 파일도 생성하고, 서브 클래스도 생성합니다. 이것은 [File] 메뉴에서 [New File...] 항목을 선택 후 "Cocoa Touch Class"를 선택하여 SecondaryViewController라는 이름을 적음으로써 할 수 있습니다. .xib 파일이 생성될 때 "Also create XIB file" 체크박스에 체크하는 것을 절대 잊지 말기를 바랍니다.

뷰 컨트롤러에 대한 소스 코드는 다음과 같습니다.

class SecondaryViewController: UIViewController {
    var text: String = ""
    @IBOutlet weak var textLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        textLabel.text = text
    }
}
@interface SecondaryViewController: UIViewController {
    @property (nonatomic, weak) NSString * text;
}
@implementation SecondaryViewController {
    @synthesize text
    
    -(void)viewDidLoad {
        [super viewDidLoad];
        [self textLabel.setText: [self text]];
    }
}

 

.xib 파일에서 뷰(view)에 UILabel 요소를 추가한 다음 그 요소를 textLabel에 연결합니다. 그러면 여러분은 뷰 컨트롤러에 textLabel라는 이름의 프로퍼티도 가지고 있고, 그 프로퍼티에 연결된 Label 요소도 갖게 됩니다.

위 소스 코드에서도 보다시피 viewDidLoad() 메소드에서 textLabel에 있는 text 프로퍼티는 SecondViewController에 있는 text로부터 할당된 값입니다.

다음 코드에서는 실제로 데이터를 전해주고 있습니다. MainViewController에 다음과 같은 메소드를 추가합니다.

// Swift
@IBAction func onButtonTap() {
    let secondaryViewController(nibName: "SecondaryViewController", bundle: nil)
    secondaryViewController.text = "Lorem Ipsum"
    navigationController?.pushViewController(secondaryViewController, animated: true)
}
// Objective-C
-(IBAction)onButtonTap {
    SecondaryViewController * secondaryViewController = [[SecondaryViewCollector alloc] initWithNibName: "SecondaryViewController" 
                                                                                                 bundle: nil];
    [secondaryViewController setText: @"Lorem Ipsum"]
    [[self navigationController] pushViewController: secondaryViewController
                                           animated: TRUE];
}

MainViewController의 인터페이스에서 버튼을 추가하고 위의 액션에 이를 연결시킨다면, 버튼을 클릭했을 때 코드가 실행될 것입니다. 위 코드를 통해 어떤 일이 벌어지는지를 살펴보겠습니다.

secondaryViewController라는 이름의 변수를 선언하고 여기에 SecondaryViewController 클래스형 인스턴스를 대입합니다. 이 때 여러분은 뷰 컨트롤러가 올바른 XIB 파일을 사용하고 있음을 보증하기 위하여 생성자를 호출할 때 XIB의 정확한 이름을 명시하게 됩니다.

그 다음 secondaryViewControllertext 프로퍼티에 문자열을 대입합니다. 이 때가 뷰 컨트롤러 사이에 실질적으로 데이터를 교환하는 순간입니다!

마지막으로 pushViewController(_:animated:)를 사용하여 내비게이션 스택에 새로 만들어진 뷰 컨트롤러를 푸쉬(push)합니다. 이 때 발생하는 화면 변화에는 애니메이션이 지정되어 있으므로 여러분은 새로 만들어진 뷰 컨트롤러가 단말기 화면의 우측에서 미끄러져 들어오는(slide in) 효과를 보게 될 것입니다.

프로퍼티를 사용하여 뷰 컨트롤러 A에서 뷰 컨트롤러 B로 데이터를 전달하는 방법에 대해 다시 한 번 정리해보겠습니다.

  • 제1단계. 뷰 컨트롤러 B(데이터를 받아서 나중에 나타나는 화면)에서 데이터를 받기 위한 프로퍼티를 선언합니다. 위 예제에서 그 프로퍼티는 text라는 이름을 가졌습니다.
  • 제2단계. 뷰 컨트롤러 B에서 데이터가 변경될 때 수행할 작업을 기술합니다. 위 예제에서 여러분은 text를 통해 받은 문자열을 label로 출력시켰습니다.
  • 제3단계. 뷰 컨트롤러 A(먼저 나타나 있고 데이터를 넘겨 줄 화면)에서 뷰 컨트롤러 B에 대한 새 인스턴스를 만든 다음 프로퍼티에 소정의 값을 설정합니다. 그리고 내비게이션 스택으로 푸쉬(push)합니다.

여기까지가 프로퍼티를 사용한 방법이었습니다. 이어서 스토리보드에서 세그웨(segue)를 사용하여 동일한 기능을 구현해보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
Cocoa (Touch)에서 애니메이션 구현하기
Cocoa Touch에서 애니메이션 구현하는 방법을 정리해 본다. UIView.animate를 사용하여 애니메이션을 지정하기 UIView.animate에는 몇 가지 오버로드들이 있지만 하나의 애니메이션에 대해 섬세하게 지정할 수 있는 메소드를 기준으로 다음과 같이 사용할 수 있다. // Swift UIView.animate(withDuration: /* 총 소요시간 */, delay: /* 애니메이션 시작 전 뜸 들이는 시간 */, options: /* 애니메이션이 실행되는 동안 속도 변화 및 각종 옵션 */, animations: { /* 애니메이션 끝에 가서 나타날 효과를 기술한다. */ /* 그러면 애니메이션을 통해 서서히 여기서 기술한대로 각종 요소들이 변할 것이다. */ }, completion..
Language/Objective-C & Swift
2021. 9. 13. 20:21

Cocoa (Touch)에서 애니메이션 구현하기

Language/Objective-C & Swift
2021. 9. 13. 20:21

Cocoa Touch에서 애니메이션 구현하는 방법을 정리해 본다.

 

UIView.animate를 사용하여 애니메이션을 지정하기

UIView.animate에는 몇 가지 오버로드들이 있지만 하나의 애니메이션에 대해 섬세하게 지정할 수 있는 메소드를 기준으로 다음과 같이 사용할 수 있다.

 // Swift
UIView.animate(withDuration: /* 총 소요시간 */,
                      delay: /* 애니메이션 시작 전 뜸 들이는 시간 */,
                    options: /* 애니메이션이 실행되는 동안 속도 변화 및 각종 옵션 */,
                 animations: {
                                 /* 애니메이션 끝에 가서 나타날 효과를 기술한다. */
                                 /* 그러면 애니메이션을 통해 서서히 여기서 기술한대로 각종 요소들이 변할 것이다. */
                             },
                 completion: { finished in
                                 /* 애니메이션이 끝난 뒤 수행할 작업을 기술한다. */
                                 /* 레퍼런스에 따르면 이 클로저는 Bool형 변수를 하나 받게 되는데, */
                                 /* 진짜로 애니메이션이 다 끝나서 호출되는 거라면 true가 전달된다. */
                                 /* 그냥 한 번 호출되었다면 false이다. */
                             })

 

 // Objective-C
[UIView animateWithDuration: /* 총 소요시간 */
                      delay: /* 애니메이션 시작 전 뜸 들이는 시간 */
                    options: /* 애니메이션이 실행되는 동안 속도 변화 및 각종 옵션 */
                 animations:^{
                                /* 애니메이션 끝에 가서 나타날 효과를 기술한다. */
                                /* 그러면 애니메이션을 통해 서서히 여기서 기술한대로 각종 요소들이 변할 것이다. */
                            }
                 completion:^(bool finished) {
                                /* 애니메이션이 끝난 뒤 수행할 작업을 기술한다. */
                                /* 레퍼런스에 따르면 이 클로저는 Bool형 변수를 하나 받게 되는데, */
                                /* 진짜로 애니메이션이 다 끝나서 호출되는 거라면 true가 전달된다. */
                                /* 그냥 한 번 호출되었다면 false이다. */
                            }];

 

UIViewPropertyAnimator를 사용하여 애니메이션을 지정하기

UIViewPropertyAnimator는 iOS 10 이후부터 도입된 클래스이다.

var animator: UIViewPropertyAnimator!
// animator 인스턴스를 초기화할 수 있는 메소드 안에서 다음과 같은 내용을 작성한다.
// 생성자 한 줄에서 많은 요소들을 한 번에 초기화 할 수도 있지만, 
// 몇 줄에 걸쳐서 하나씩 지정해 보기 위해 가장 간단한 생성자를 사용한다.
// duration은 생성자에서만 지정 가능하며 이후 프로퍼티에서는 읽기 전용이므로 변경 불가하다.
// curve도 생성자에서만 지정 가능하다.
// animations는 나중에 추가할 수 있으므로 생성자에서는 nil을 지정한다.
animator = UIViewPropertyAnimator(duration: /* 총 소요시간 */,
                                     curve: /* 애니메이션이 실행되는 동안 속도 변화 및 각종 옵션 */,
                                animations:nil)
animator.startAnimation(afterDelay: /* 애니메이션 시작 전 뜸 들이는 시간 */)
animator.addAnimations({
    /* 애니메이션 끝에 가서 나타날 효과를 기술한다. */
    /* 그러면 애니메이션을 통해 서서히 여기서 기술한대로 각종 요소들이 변할 것이다. */
})
animator.addCompletion({ position in
    /* 애니메이션이 끝난 뒤 수행할 작업을 기술한다. */
    /* 레퍼런스에 따르면 이 클로저는 UIViewAnimatingPosition형 변수를 하나 받게 되는데, */
    /* 이름 그대로 애니메이션의 주기에 따라 .start, .current, .end 중 하나이다. */
})

 

카테고리 “Language/Objective-C & Swift”
more...
Swift와 Kotlin, 각 언어에서 비동기 실행하기(2)
본 게시물은 Ivan Fytsyk님의 게시글 Asynchronous execution in Kotlin and Swift: Concurrency problems and how to solve them을 바탕으로 작성되었습니다. Swift와 Kotlin, 각 언어에서 비동기 실행하기 본 게시글에서는 모바일 앱 개발의 양대 언어인 Swift와 Kotlin에서 비동기 실행을 하는 방법에 대해 정리합니다. 이전 글에서 우리는 비동기 실행의 기본적인 예를 살펴보았습니다. 여러분은 아마도 변경 가능한 공유 상태(shared mutable state)와 교착상태(dead-lock)라는 두 개의 일반적인 문제에 대해 이미 알고 있을 것입니다. 이러한 문제는 양대 언어에서도 공통된 문제이기에 서로 비슷한 해결책을 가지고..
Language/Objective-C & Swift
2021. 9. 11. 22:09

Swift와 Kotlin, 각 언어에서 비동기 실행하기(2)

Language/Objective-C & Swift
2021. 9. 11. 22:09

본 게시물은 Ivan Fytsyk님의 게시글 Asynchronous execution in Kotlin and Swift: Concurrency problems and how to solve them을 바탕으로 작성되었습니다.

 

Swift와 Kotlin, 각 언어에서 비동기 실행하기

본 게시글에서는 모바일 앱 개발의 양대 언어인 Swift와 Kotlin에서 비동기 실행을 하는 방법에 대해 정리합니다.

 

이전 글에서 우리는 비동기 실행의 기본적인 예를 살펴보았습니다. 여러분은 아마도 변경 가능한 공유 상태(shared mutable state)와 교착상태(dead-lock)라는 두 개의 일반적인 문제에 대해 이미 알고 있을 것입니다. 이러한 문제는 양대 언어에서도 공통된 문제이기에 서로 비슷한 해결책을 가지고 있습니다. 지금부터 살펴보겠습니다.

 

Shared Mutable State

Shared mutable state부터 시작해 보겠습니다. 아래 예에서 우리는 어떤 정수 값을 비동기식으로 1,000회 증가시켜보겠습니다.

import kotlinx.coroutines.*
import org.junit.Test

class ConcurrencyProblemTest {
    private var someValue = 0
    
    fun runCodeWithConcurrencyProblem() = runBlocking {
        for (i in 0 .. 999) {
            GlobalScope.launch() {
                someValue = someValue + 1
            }
        }
        delay(2000)
        print(someValue)
    }
}

 

import UIKit
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

var someValue = 0

for _ in 0 ... 999 {
    DispatchQueue.global().async {
        someValue = someValue + 1
    }
}

sleep(2)
print(someValue)

 

예상했다시피 이 값은 1,000이 되지 못하고 때때로 달라집니다. 이러한 Shared Mutable State의 해결책은 간단합니다. 변경 가능한 변수의 읽기와 쓰기를 동기화시키면 됩니다. 즉 읽기와 쓰기 작업은 주로 실행되는 쓰레드에서 직렬로(serially) 실행되어야 합니다.

import kotlinx.coroutines.*
import org.junit.Test

class ReadingWritingSerialTest {
    private var someValue = 0
    val singleThreadContext = newSingleThreadContext("mySerialContext")
    
    private fun incrementValue() {
        GlobalScope.launch(singleThreadContext) {
            someValue = someValue + 1
        }
    }
    
    fun runCodeWithConcurrencyProblem() = runBlocking {
        for (i in 0 .. 999) {
            GlobalScope.launch() {
                incrementValue()
            }
        }
        delay(2000)
        print(someValue) // result is always 1,000
    }
}

 

import UIKit
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

var someValue = 0
let serialQueue = DispatchQueue(label: "mySerialQueue")

func incrementValue() {
    serialQueue.async {
        someValue = someValue + 1
    }
}

for _ in 0 ... 999 {
    DispatchQueue.global.sync {
        incrementValue()
    }
}

sleep(2)
print(someValue) // always prints 1,000

 

여기서 여러분은 우리가 작성한 비동기 코드를 동기 코드로 변환하였고, 이로 인해 멀티쓰레드 실행의 이점을 얻을 수 없게 됨을 알았을 것입니다. 맞습니다. 왜냐하면 mutable 변수의 값을 변경하는 것 외에 아무것도 하지 않았기 때문입니다. 하지만 실전 코드에서는, 백그라운드에서 실행될 필요가 있는 연산 및 그 결과는 동기 방식으로 보관하는 연산으로 이루어진 부분들이 많을 수 있습니다.

 

Dead-lock

두 번째 문제는 교착 상태(dead-lock)입니다. 일반적으로 어떤 코드 블록이 내부의 코드 블록에 기술된 내용이 종료될 때까지 기다리나, 내부의 코드 블록 또한 상위의 코드가 종료될 때까지 기다리고 있을 때 쿄착 상태는 발생합니다.

import kotlinx.coroutines.*
import org.junit.Test

class DeadLockTest {
    val singleThreadContext = newSingleThreadContext("mySerialContext")
    
    @Test
    fun runCodeWithConcurrencyProblem() = runBlocking {
        runDeadLock()
        delay(2000)
    }
    
    fun runDeadLock() {
        GlobalScope.launch(singleThreadContext) {
            println("Blocking coroutine start")
            runBlocking(singleThreadContext) {
                // 바깥 블록은 안쪽 블록이 완료될 때까지 기다리겠으나,
                // 안쪽 블록도 마찬가지로 바깥 블록이 완료될 때까지 기다립니다.
                println("Unreachable statement")
            }
            println("Unreachable statement")
        }
    }
}

 

import UIKit
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let serialQueue = DispatchQueue(label: "mySerialQueue")

serialQueue.sync {
    print("Blocking block starts")
    serialQueue.sync {
        print("Unreavhable statement")
    }
}

 

이들 예제에서는 하나의 싱글 쓰레드 큐와 두 블록의 코드가 사용되었습니다. 첫 번째 블록에서 우리는 동기식으로 두 번째 블록을 시작하였고 새로운 블록의 실행이 끝날 때까지 첫 번째 블록은 일시중지됩니다. 하지만 두 번째 블록도 실행되지 못할 것입니다. 왜냐하면 두 번째 블록은 작업 큐의 맨 마지막에 추가되었기 때문에 큐에서 앞쪽 순서에 놓인 블록이 종료될 때까지는 두 번째 블록도 큐에서 순서가 옮겨지지 못하기 때문입니다. 그러한 종류의 교착 상태를 피하기 위한 필자의 조언은 다음과 같습니다.

중첩된 비동기 코드 블록을 실행할 때 동일한 큐를 사용하지 마세요.

import kotlinx.coroutines.*
import org.junit.Test

class DeadLockResolutionTest {
    val singleThreadContext1 = newSingleThreadContext("mySerialContext1")
    val singleThreadContext2 = newSingleThreadContext("mySerialContext2")
    
    @Test
    fun runCodeWithConcurrencyProblem() = runBlocking {
        runDeadlock()
        delay(2000)
    }
    
    fun runDeadLock() {
        GlobalScope.launch(singleThreadContext1) {
            println("Blocking coroutine start")
            runBlocking(singleThreadContext2) {
                // 바깥 블록은 별도의 쓰레드에서 실행되고 있기에,
                // 다음 문장은 접근이 가능합니다.
                println("Unreachable statement")
            }
            println("Unreachable statement")
        }
    }
}

 

import UIKit
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let serialQueue1 = DispatchQueue(label: "mySerialQueue1")
let serialQueue2 = DispatchQueue(label: "mySerialQueue2")

serialQueue1.sync {
    print("Blocking block starts")
    serialQueue2.sync {
        print("Unreachable statement") // 이미 접근 가능하게 되었습니다.
    }
    print("Unreachable statement")
}

 

여기까지가 필자가 접해보았던 일반적으로 흔한 동시성 문제였습니다. 여러분도 보시다시피 동시성 문제와 그에 대한 해법은 양대 언어에서 매우 비슷하게 보입니다. 읽어주셔서 감사합니다.

카테고리 “Language/Objective-C & Swift”
more...
Swift와 Kotlin, 각 언어에서 비동기 실행하기(1)
본 게시물은 Ivan Fytsyk님의 게시글 Asynchronous execution in Kotlin and Swift을 바탕으로 작성되었습니다. Swift와 Kotlin, 각 언어에서 비동기 실행하기 본 게시글에서는 모바일 앱 개발의 양대 언어인 Swift와 Kotlin에서 비동기 실행을 하는 방법에 대해 정리합니다. 이번 시간에 우리는 Kotlin과 Swift에서 동시성 모델(concurrency model)을 비교하며 설명해 볼 것입니다. 이 지식은 Android 및 iOS 프레임워크를 모두 배워보고자 원하는 분들에게도 유익할 것이고, 이미 양대 플랫폼에서 개발을 해 본 분들에게도 유용할 것입니다. 더 많은 정보는 필자의 저서를 통해 확인할 수 있습니다. 기본 용어들 우리가 다루어보고자 하는 기본..
Language/Objective-C & Swift
2021. 9. 11. 21:11

Swift와 Kotlin, 각 언어에서 비동기 실행하기(1)

Language/Objective-C & Swift
2021. 9. 11. 21:11

본 게시물은 Ivan Fytsyk님의 게시글 Asynchronous execution in Kotlin and Swift을 바탕으로 작성되었습니다.

 

Swift와 Kotlin, 각 언어에서 비동기 실행하기

본 게시글에서는 모바일 앱 개발의 양대 언어인 Swift와 Kotlin에서 비동기 실행을 하는 방법에 대해 정리합니다.

 

이번 시간에 우리는 Kotlin과 Swift에서 동시성 모델(concurrency model)을 비교하며 설명해 볼 것입니다. 이 지식은 Android 및 iOS 프레임워크를 모두 배워보고자 원하는 분들에게도 유익할 것이고, 이미 양대 플랫폼에서 개발을 해 본 분들에게도 유용할 것입니다. 더 많은 정보는 필자의 저서를 통해 확인할 수 있습니다.

 

기본 용어들

우리가 다루어보고자 하는 기본 용어들에는 쓰레드(Thread), 오퍼레이션(Operation) 및 큐(Queue)가 있습니다.

 

쓰레드(Thread)

쓰레드(Thread)는 동시에 어떤 연산을 실행할 목적으로 멀티코어 CPU를 사용하는 저수준의 추상화(abstraction)입니다. 싱글코어 CPU에서는 각 연산들 사이를 빠르게 전환함으로써 병렬 실행을 에뮬레이트(emulate)할 수 있습니다. 오늘날 Kotlin이나 Swift에서는 쓰레드가 직접적으로 사용되는 경우가 흔치 않기 때문에 이 글에서 우리는 쓰레드에 대해서는 다루지 않겠습니다. 새로운 쓰레드를 생성하고 이들을 관리하는 것은 많은 시스템 자원을 요구하기 때문에, 현대적인 개발 환경에서 우리는 일반적으로 미리 정의된 몇몇의 쓰레드 풀(Thread Pool)만을 사용하고 (이를 위해) 오퍼레이션(Operation)과 큐(Queue)를 가지고 작업하게 될 것입니다.

 

오퍼레이션(Operation)

오퍼레이션(Operation)은 단순히 한 블록의 코드입니다. 일반적으로 클로저(closure)라든가 다른 객체지향(OOP)적 추상화로써 표현됩니다(예를 들어 Swift에서는 DispatchItem으로 불리고 Kotlin에서는 Task로 불립니다.

 

큐(Queue)

큐(Queue)는 오퍼레이션(Operation)들의 선입선출형 콜렉션(collection)입니다. (이 콜렉션에 담긴) 각각의 오퍼레이션들은 순서대로 선택되어 나온 뒤 각기 다른 쓰레드에서 실행될 수 있습니다.

 

큐에 새로운 오퍼레이션을 추가하는 것은 크게 두 가지 방법으로써 가능합니다. 동기식(sync)과 비동기식(async)이 그것입니다. 동기식은 새로 시작된 오퍼레이션이 끝날 때까지 현재의 오퍼레이션(큐에 오퍼레이션을 넣고자 하는 지금의 작업 흐름)이 일시중지될 것임을 의미합니다. 비동기식은 새로운 오퍼레이션이 시작되더라도 현재의 오퍼레이션은 일시중지되지 않고 나머지 작업을 계속 실행될 것임을 의미합니다.

 

예제 1

설명은 여기까지만 하고, 지금부터는 코드들을 많게 보면서 Kotlin과 Swift에서 위의 개념들이 어떻게 구현되어 있는지를 확인해 보겠습니다. 예제 코드를 실행해보기 위해 Android Studio Kotlin REPL과 Xcode Playground를 사용할 것입니다.

먼저 Kotlin의 코드입니다.

import kotlinx.coroutines.experimental.*

runBlocking {
    GlobalScope.async { // Kotlin sample
        delay(1000L) // 1 second suspending
        println("async call")
    }
    delay(2000L)
}

 

다음은 Swift의 코드입니다.

DispatchQueue.global().async { // Swift sample
    sleep(1) // 1 second suspending
    print("async call")
}

 

여기서 어떤 일들이 벌어지는지 설명해보겠습니다. 먼저 Kotlin 측 코드를 보면 runBlocking { ... }이라는 부분이 보입니다. 이것은 블로킹 함수(blocking function)으로서 REPL의 메인 쓰레드(main threa)가 종료되기 전에 비동기 호출의 결과를 우리가 볼 수 있도록 해 줍니다. 이것은 단지 우리가 코드의 실행 결과를 보는 데 도움을 주기 위해 도입된 구조일 뿐입니다. Xcode Playground는 이와 유사한 구조가 없어도 작동이 됩니다.

 

예제 2

그 다음 우리가 관심을 두어야 할 것은 GlobalScopeDispatchQueue.global()입니다. 이들은 "미리 정의되어 있고 현재 작동 중인 쓰레드" 및 필자가 앞서 언급했던 오퍼레이션 큐(operation queue)를 캡슐화하고 있습니다. 대부분의 경우 이것으로 우리가 소스 코드들을 비동기식으로 실행하는 데 충분합니다. 보다 특수한 목적을 위해 우리는 직접 사용자 정의 큐를 만들 수도 있습니다.

백그라운드에서 실행이 오래 걸리는 작업들이 완료되고 난 뒤, 보통 우리는 UI 쓰레드에게 이 실행 결과를 전달하기도 합니다. Kotlin에서는 이것이 어떻게 이루어지는지 보겠습니다.

import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.Main

val job = GlobalScope.async {
    delay(1000L) // 1 second suspending
    println("async call")
    GlobalScope.async(context = Dispatchers.Unconfined) { 
        // 실제 Android 어플리케이션에서는 Dispatchers.Main으로 대체합니다.
        println("called in UI thread")
    }
}

runBlocking {
    job.join()
}

 

Swift에서는 백그라운드 작업 실행 완료를 UI 쓰레드에게 어떻게 통지하는지 보겠습니다.

DispatchQueue.global().async {
    sleep(1) // 1 second suspending
    print("async call")
    DispatchQueue.main.async {
        print("call in UI thread")
    }
}

 

여기서 우리는 비동기 블록 실행이 완료되는 것을 기다리기 위하여 job.join()을 사용하여 UI 쓰레드를 대기시켰습니다. 앞서 적었던 예제에서 delay(2000L)을 호출했던 것을 대체한 것에 지나지 않습니다.

비동기 코드 내에서도 다시 일부 코드 블록을 UI 쓰레드에서 실행하기 위하여 우리는,
Kotlin에서는 GlobalScope.async(context = Dispatchers.Unconfined) { ... }이라는 미리 정의된 메인 큐(main queue)가 동반된 생성자를 사용하였고,
Swift에서는 DispatchQueue.main.async { ... }이라는 호출을 사용하였습니다.

여기까지가 모바일 개발에서 사용되는 동시성 구현의 가장 쉬운 예였습니다. 이런 구조라면 백그라운드에서는 무거운 연산이나 데이터 처리를 수행하고 그 결과만 UI 쓰레드에 전달하면 되겠지요. 다음 장에서 우리는 오퍼레이션과 큐를 가지고 할 수 있는 작업들을 해 보고, 우리가 직접 큐를 만들어 보기도 할 것이며 교착상태(deadlock)나 경쟁(race)과 같은 동시성 문제를 어떻게 해결할 것인지에 대해 살펴보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
@property의 특성: nonatomic, retain, strong, weak 등
본 게시물은 Saurav Satpathy님의 게시글 Attributes of @property : nonatomic, retain, strong, weak etc..을 바탕으로 작성되었습니다. @property의 특성: nonatomic, retain, strong, weak 등 본 게시글은 프로퍼티 특성(property attribute)에 대한 참고 자료로서 작성되었습니다. 기본적으로 @property는 다음과 같이 생겼습니다. @property (atomic, readonly, strong) NSString * name; 또는 다음과 같이 생겼습니다. @property (atomic, readwrite, assign) NSInteger age; 1. 접근 특성(access attributes) (1..
Language/Objective-C & Swift
2021. 8. 20. 11:35

@property의 특성: nonatomic, retain, strong, weak 등

Language/Objective-C & Swift
2021. 8. 20. 11:35

본 게시물은 Saurav Satpathy님의 게시글 Attributes of @property : nonatomic, retain, strong, weak etc..을 바탕으로 작성되었습니다.

 

@property의 특성: nonatomic, retain, strong, weak 등

본 게시글은 프로퍼티 특성(property attribute)에 대한 참고 자료로서 작성되었습니다.

 

기본적으로 @property는 다음과 같이 생겼습니다.

@property (atomic, readonly, strong) NSString * name;

또는 다음과 같이 생겼습니다.

@property (atomic, readwrite, assign) NSInteger age;

 

1. 접근 특성(access attributes)

(1-1) readonly

name의 선언에서 보이는 바와 같이 그 프로퍼티로 어떤 값이 대입(assign)될 수 없습니다. readonly 특성이 적용된 프로퍼티 내부에는 세터(setter) 메소드가 생략되기 때문입니다.

 

(1-2) readwrite

그 프로퍼티가 갖는 값을 다른 곳으로 가져갈 수 있을 뿐만 아니라, 프로퍼티가 갖는 값이 다른 값으로 대체될 수도 있습니다.

 

2. 쓰레드 특성(threading attributes)

(2-1) atomic

어떤 프로퍼티를 atomic으로 선언하는 것은, 컴파일러로 하여금 그 객체에 대해 여러 쓰레드(thread)에서 동시 접근하는 것을 방지하는 부가적은 코드를 생성하도록 지시합니다.
이 부가적인 코드에서는 세마포어(semaphore)를 잠그고(lock), 그 프로퍼티에 대해 값을 가져오거나 설정한 다음, 다시 세마포어의 잠금을 해제(unlock)합니다. 세마포어를 잠그거나 잠금 해제하는 것은 비용이 들지만(물론 보통은 이 정도의 비용은 경미한 편이지만), 그 프로퍼티의 값을 가져오거나 설정하는 과정이 온전하게 수행됨을 보장합니다.

 

(2-2) nonatomic

프로퍼티 내부의 접근 메소드가 그 값을 단순히 반환하거나 수정하도록 지시합니다. 서로 다른 쓰레드로부터 동시에 이 값에 접근해 올 경우 어떤 일이 벌어질 지에 대해서는 보장이 되지 않습니다. 이런 이유로 인해 atomic으로 선언된 프로퍼티보다 nonatomic으로 선언되는 프로퍼티가 (안전성은 낮지만) 접근 속도가 빠릅니다.

 

3. 메모리 관리 특성(memory management attributes) 및 수명 한정자(lifetime qualifiers)

많은 언어들은 가비지 콜렉터(garbage collector)를 통해 메모리 관리 기능을 지원하지만, Objective-C에서는 객체 소유(object ownership)레퍼런스 카운트(reference counting)이라고 불리는 보다 효율적인 대체 수단을 사용합니다.

 

(3-1) strong과 retain

어떤 프로퍼티를 strong으로 선언한다는 것은 그 프로퍼티가 참조하고자 하는 객체를 “소유(own)”한다는 뜻입니다. 이렇게 선언된 프로퍼티에 여러분이 어떤 데이터를 대입시키면, 그 프로퍼티가 strong 참조로써 다른 데이터를 참조하게 되지 않는 한, 그 데이터는 파괴되지 않을 것입니다.

Objective-C에서 한 객체는 다른 객체들로부터 적어도 한 번은 참조되고 있다면 그 객체는 계속하여 유효한 상태로 남아 있습니다.

non-ARC 코드에서 strong은 단지 retain과 같은 말일 뿐입니다.

어떤 변수가 있을 때 그 변수가 스코프(scope)에서 유효할 동안 또는 그 변수가 다른 객체나 nil을 참조할 때까지 그 변수는 가리키고 있는 객체와 strong 참조를 유지하고 있습니다.

// Example
@property (nonatomic, strong) NSArray * listOfStudents;

 

(3-2) weak

weak은 “누군가가 그 객체를 strong 참조를 하는 한 그 객체를 메모리에 남아있게 하라”는 의미를 갖고 있으며 여러분이 그 객체의 수명에 관여하지 않을 때 지정합니다. 여러분이 weak 참조로 어떤 객체를 참조하면, 그 객체는 다른 객체에 의해 strong 참조되고 있는 한 weak 참조하고 있는 이 쪽에서도 계속 유효하지만, 어떤 객체도 참조하지 않게 될 때 weak 참조로 참조하던 객체는 파괴(destroy)됩니다.

델리게이트 형식의 프로퍼티(delegate property)는 순환적으로 retain되는 것을 방지하기 위해 weak 참조가 됩니다.

IBOutlet이나 서브 뷰(Subview)는 슈퍼 뷰(Superview)에 의해 strong 참조되고 있기 때문에 다른 코드에서는 weak 참조가 됩니다.

// Example
@property (nonatomic, weak) id<SampleDelegate> sDelegate;
@property (nonatomic, weak) IBOutlet UILabel * titleLabel;

 

(3-3) assign

새 값으로서 프로퍼티에 대입됩니다. 이 특성은 대개 int, float, NSInteger 등의 프리미티브 타입(primitive type)의 프로퍼티에 적용됩니다.

 

(3-4) copy

객체가 수정 가능(mutable)할 때 이 특성이 적용됩니다. 프로퍼티에 참조될 객체가 지금 이 순간의 상태로 참조되길 원하며 이후에 다른 요소에 의해 변경되는 것을 여기에서 반영하고 싶지 않을 때 이 특성을 사용하면 됩니다. 다만 이 프로퍼티에 참조되는 객체는 원본에 대한 복사본으로서 현재의 프로퍼티에 의해 retain된 상태이기 때문에, 사용이 끝났다면 여러분이 직접 해제(release)해야 합니다.

이 특성이 적용된 프로퍼티로 객체가 참조될 때 원본과는 다른 메모리 위치에 복사가 이뤄지며 그 복사본의 참조 횟수(retain count)가 증가된 후 해당 프로퍼티에 참조됩니다. 이 참조 횟수는 여러분이 그 객체를 더 이상 소유하고 있지 않을 때 자동으로 감소되겠으나, non-ARC 환경이라면 여러분이 직접 명시적으로 해제해 주어야 합니다.

 

참고 여러분이 mutable 프로퍼티에 대해 copy 특성을 사용하여 선언하였다면, 여러분이 이를 mutable이라 선언했어도 여전히 immutable입니다. 여러분이 여기에 어떤 값이나 객체를 가감하려 한다면 예외가 throw될 것입니다.

 

(3-5) unsafe_unretained

이 특성은 Cocoa 및 Cocoa Touch의 클래스 중 weak 참조를 지원하지 않는 극히 일부 클래스에 대해 사용됩니다. 여러분이 그러한 클래스 형식의 객체에 대해 정보를 얻고자 할 때 weak 참조의 로컬 변수나 weak 참조의 프로퍼티를 사용할 수 없음을 의미합니다. 이 특성은 참조된 객체를 계속해서 유효한 상태로 유지하는 능력이 없으면서 그 객체에 대한 strong 참조가 없어져도 이 특성을 갖는 프로퍼티는 nil 참조로 바뀌지 않는다는 뜻입니다. 때문에 이 특성을 가진 프로퍼티가 가리키는 객체가 할당 해제(deallocated)되었다면, 이 프로퍼티가 갖는 포인터는 오류입니다.

 

4. getter=

직접 작성한 메소드를 프로퍼티에 대한 getter로서 사용할 때 이 특성을 적용합니다.

 

5. setter=

직접 작성한 메소드를 프로퍼티에 대한 setter로서 사용할 때 이 특성을 적용합니다.

 

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[iOS・iPadOS] iOS 앱에서도 안드로이드처럼 DrawerLayout(사이드 메뉴) 만들기
iOS・iPadOS 이전 게시글: 앱에 커스텀 폰트(custom font) 적용하기. iOS 앱에서도 안드로이드처럼 DrawerLayout(사이드 메뉴) 만들기 안드로이드에서는 라이브러리 차원에서 DrawerLayout, 일명 사이드 메뉴 또는 햄버거(hamburger) 버튼에 해당하는 메뉴 인터페이스가 지원되지만, iOS 쪽에는 그와 똑같은 기능이 따로 없다. iOS에서 DrawerLayout에 해당하는 인터페이스를 만들려면 써드 파티 라이브러리를 끌어오거나 다른 곳에서 코드를 복사/붙여넣기하는 방법 등 여러가지 길이 있겠으나 여기에서는 별도 라이브러리 없이 이와 유사한 인터페이스를 만들어보도록 하겠다. 1 단계. 내비게이션 뷰 컨트롤러(Navigation View Controller) 구성하기 앱 화..
API/Cocoa
2021. 8. 15. 15:10

[iOS・iPadOS] iOS 앱에서도 안드로이드처럼 DrawerLayout(사이드 메뉴) 만들기

API/Cocoa
2021. 8. 15. 15:10

iOS・iPadOS

이전 게시글: 앱에 커스텀 폰트(custom font) 적용하기.

 

iOS 앱에서도 안드로이드처럼 DrawerLayout(사이드 메뉴) 만들기

안드로이드에서는 라이브러리 차원에서 DrawerLayout, 일명 사이드 메뉴 또는 햄버거(hamburger) 버튼에 해당하는 메뉴 인터페이스가 지원되지만, iOS 쪽에는 그와 똑같은 기능이 따로 없다. iOS에서 DrawerLayout에 해당하는 인터페이스를 만들려면 써드 파티 라이브러리를 끌어오거나 다른 곳에서 코드를 복사/붙여넣기하는 방법 등 여러가지 길이 있겠으나 여기에서는 별도 라이브러리 없이 이와 유사한 인터페이스를 만들어보도록 하겠다.

 

1 단계. 내비게이션 뷰 컨트롤러(Navigation View Controller) 구성하기

앱 화면의 상단에 햄버거 버튼을 만들어야 하므로 앱의 인터페이스에 내비게이션을 적용한다. Xcode에서 기본으로 만들어 준 뷰 컨트롤러를 그대로 둔 채 [Editor] - [Embed In] - [Navigation Controller]를 클릭한다.

앱 인터페이스에 내비게이션을 적용한다.

 

Entry Point와 빈 화면 사이에 Navigation Controller가 새로 끼어들면서 기존의 빈 화면은 두 번째로 밀려났다. 이 두 번째 화면에 DrawerLayout과 비슷한 레이아웃을 구성한다. 다음과 같이 최상위 뷰(View)의 아래에 두 개의 뷰(View)를 두는데 하나의 뷰에는 기본적인 화면을 구성하고 또 다른 뷰에는 사이드 메뉴를 구성한 다음 그 두 개의 뷰를 포갠다. 사이드 메뉴로 쓰일 뷰의 폭은 다소 좁게 지정할 수 있는데 여기서는 300으로 지정한다. 좀 더 확실한 구별을 위해 사이드 메뉴로 쓰일 뷰의 배경은 오렌지 색으로 칠했다.

DrawerLayout을 고려하여 레이아웃을 작성한다.

그 다음 네비게이션 바의 좌측에 햄버거 버튼을 추가한다. 햄버거 버튼에 사용할 이미지를 구해서 넣는 과정까지는 생략한다. 혼동하지 말아야 할 것은 내비게이션 바에 추가되는 버튼의 형식은 UIBarButtonItem이라는 것이다. 이것을 내비게이션 바의 좌측에 추가한다.

내비게이션 바에 Bar Button Item을 추가한다(1).
내비게이션 바에 Bar Button Item을 추가한다(2).
내비게이션 바에 Bar Button Item을 추가한다(3).

 

2 단계. 로직 구성하기

겉 모습은 대강 위와 같이 만들어졌으므로 이제 코딩을 할 차례이다. 햄버거 버튼을 클릭할 때마다 사이드 메뉴가 나타나거나 사라지거나 해야 하는데, 이 방법에는 여러 가지가 있겠으나 여기에서는 constraint를 사용해서 사이드 메뉴의 폭을 유지한 채로 좌우로 이동시키는 방식을 사용하겠다. 다시 적으면, 평소에는 사이드 메뉴가 화면 바깥으로 완전히 나가 있다가 햄버거 버튼을 누르면 화면에 나타나도록 constraint를 조정하고 다시 햄버거 버튼을 누르면 사이드 메뉴를 화면 바깥으로 완전히 이동시키는 방식이다.

사이드 메뉴가 화면 안으로 들어왔을 때의 leading constraint.
사이드 메뉴가 화면 밖으로 벗어났을 때의 leading constraint. 

 

이와 같은 작동을 구현하기 위해 다음과 같이 최소한 2개의 IBOutlet과 1개의 IBAction이 필요하다.

class ViewController: UIViewController {
    // sidebarView의 폭을 정의하는 상수입니다.
    let sidebarViewWidth = 300
    
    // ...

    // 사이드 메뉴를 포함하고 있는 뷰(오렌지색 배경의 뷰)입니다.
    @IBOutlet var sidebarView: UIView!
    // sidebarView와 super view 사이에 적용되고 있는 Leading Constraint입니다.
    @IBOutlet var sidebarViewLeadingConstraint: NSLayoutConstraint!

    // ...

    // 햄버거 버튼을 터치할 때마다 수행될 작동이 정의된 메소드입니다.
    @IBAction func hamburgerNavigationItemTouchDown(_ sender: Any) {
        
    }
}

 

Objective-C로는 다음과 같이 적을 수 있다.

// ViewController.m
// sidebarView의 폭을 정의하는 상수입니다.
#define SIDEBAR_VIEW_WIDTH 300
// ...
@interface ViewController ()
// 사이드 메뉴를 포함하고 있는 뷰(오렌지색 배경의 뷰)입니다.
@property(atomic) IBOutlet UIView * sidebarView;
// sidebarView와 super view 사이에 적용되고 있는 Leading Constraint입니다.
@property(atomic) IBOutlet NSLayoutConstraint * sidebarViewLeadingConstraint;
@end

@implementation ViewController
// 햄버거 버튼을 터치할 때마다 수행될 작동이 정의된 메소드입니다.
- (IBAction)hamburgerNavigationItemTouchDown:(id)sender {
    
}
@end

 

ViewController 클래스에서 선언한 총 3개의 메소드 및 변수들을 스토리보드와 reference 시킨다.

코드에서 작성한 멤버와 스토리보드 요소 사이를 reference 시킨다.

 

사이드 메뉴를 화면에 나타나도록 작동하는 코드는 다음과 같이 작성한다. 사이드 메뉴 뷰와 super view 사이에 적용되는 leading constraint 값을 0으로 하면 된다.

func showSidebarView() {
    sidebarViewLeadingConstraint.constant = 0
    
    // 애니메이션 효과
    UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: {
        self.view.layoutIfNeeded()
    })
}

 

Objective-C로는 다음과 같이 적을 수 있다.

- (void)showSidebarView {
    self.sidebarViewLeadingConstraint.constant = 0;
    
    [UIView animateWithDuration: 0.5
                          delay: 0.0
                        options: UIViewAnimationOptionCurveEaseIn
                     animations: ^{[self.view layoutIfNeeded];}
                     completion: nil];
}

 

사이드 메뉴를 화면 밖으로 치우는 코드는 다음과 같이 작성한다. 사이드 메뉴 뷰와 super view 사이에 적용되는 leading constraint의 값을 사이드 메뉴의 폭이나 더 넓은 값으로 지정하되, 음수로 지정한다.

func hideSidebarView() {
    sidebarViewLeadingConstraint.constant = -CGFloat(sidebarViewWidth)
    
    // 애니메이션 효과
    UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: {
        self.view.layoutIfNeeded()
    })
}

 

Objective-C로는 다음과 같이 적을 수 있다.

- (void)hideSidebarView {
    self.sidebarViewLeadingConstraint.constant = -(CGFloat)SIDEBAR_WIDTH;
    
    [UIView animateWithDuration: 0.5
                          delay: 0.0
                        options: UIViewAnimationOptionCurveEaseIn
                     animations: ^{[self.view layoutIfNeeded];}
                     completion: nil];
}

 

햄버거 메뉴를 클릭할 때마다 사이드 메뉴에 적용된 leading constraint의 값이 음수인지 여부를 탐지하여 보이거나 감출 수 있다.

@IBAction func hamburgerNavigationItemTouchDown(_ sender: Any) {
    if (sidebarViewLeadingConstraint.constant < 0) {
        showSidebarView()
    } else {
        hideSidebarView()
    }
}

 

Objective-C로는 다음과 같이 적을 수 있다.

- (IBAction)hamburgerNavigationItemTouchDown:(id)sender {
    if (self.sidebarViewLeadingConstraint.constant < 0) {
        [self showSidebarView];
    } else {
        [self hideSidebarView];
    }
}

 

DrawerLayout(Sidebar) 작동 영상.

 

카테고리 “API/Cocoa”
more...
썸네일 이미지
[iOS・iPadOS] 앱에 커스텀 폰트(custom font) 적용하기.
iOS・iPadOS 이전 게시글: Application의 이름 localization 적용하기. 다음 게시글: iOS 앱에서도 안드로이드처럼 DrawerLayout(사이드 메뉴) 만들기 앱에 커스텀 폰트(custom font) 적용하기. 이번 시간에는 iOS 또는 iPadOS 앱에 커스텀 폰트를 내장시킨 후 이를 적용시키는 방법에 대해 정리한다. 1 단계. 커스텀 폰트 적용 전 전・후 비교를 확실하게 보기 위하여 일단 스토리보드에 적당한 텍스트를 추가한다. 시스템 폰트가 적용되어 있고, 폰트 목록에도 아직 커스텀 폰트가 나타나지 않는다. 2 단계. 커스텀 폰트 추가 앱에서 사용하고자 하는 폰트 파일을 Xcode의 프로젝트 내비게이터(Project navigator) 안으로 복사해 넣는다. 여기에서는 무난..
API/Cocoa
2021. 8. 14. 21:44

[iOS・iPadOS] 앱에 커스텀 폰트(custom font) 적용하기.

API/Cocoa
2021. 8. 14. 21:44

iOS・iPadOS

이전 게시글: Application의 이름 localization 적용하기.

다음 게시글: iOS 앱에서도 안드로이드처럼 DrawerLayout(사이드 메뉴) 만들기

 

앱에 커스텀 폰트(custom font) 적용하기.

이번 시간에는 iOS 또는 iPadOS 앱에 커스텀 폰트를 내장시킨 후 이를 적용시키는 방법에 대해 정리한다.

 

1 단계. 커스텀 폰트 적용 전

전・후 비교를 확실하게 보기 위하여 일단 스토리보드에 적당한 텍스트를 추가한다. 시스템 폰트가 적용되어 있고, 폰트 목록에도 아직 커스텀 폰트가 나타나지 않는다.

목록에 시스템 기본 폰트들만 떠 있고, 텍스트 폰트도 기본 폰트만 적용되어 있다.

 

2 단계. 커스텀 폰트 추가

앱에서 사용하고자 하는 폰트 파일을 Xcode의 프로젝트 내비게이터(Project navigator) 안으로 복사해 넣는다. 여기에서는 무난한 나눔폰트(나눔명조)를 사용해 보겠다.

나눔명조체 파일을 Xcode 프로젝트 안으로 드래그하여 넣는다.

 

3 단계. 번들에 커스텀 폰트 인식

앱에 폰트가 추가되었음을 번들(bundle) 차원에서 알고 있어야 한다. 이를 위해 ① 프로젝트 내비게이터(Project navigator) - ② 최상단 항목 - ③ TARGET에서 해당 항목을 선택한 후 ④ Info 탭을 클릭한다. Custom iOS Target Properties에서 ⑤ [+] 모양의 버튼을 클릭하여 ⑥ "Fonts provided by application"이라는 이름이 붙은 변수를 새로 추가한다.

번들에게 새 폰트가 추가되었음을 알린다(1).

 

새로 추가할 폰트의 수만큼 Item을 추가한 다음 각 Item마다 폰트 파일의 이름을 정확하게 적는다.

번들에게 새 폰트가 추가되었음을 알린다(2).

 

4 단계. UI 요소에 커스텀 폰트 적용

그 다음 스토리보드로 돌아가서 커스텀 폰트를 적용한다. 폰트를 적용할 대상을 클릭하고 폰트 유형을 System에서 Custom으로 바꾼 다음, 폰트 이름을 클릭하여 폰트 목록을 연다.

UI 요소에 커스텀 폰트를 적용한다(1).

 

폰트 목록에서 앞의 1단계에서는 없었던 폰트가 추가된 것을 확인할 수 있다. 이것으로 앱에 커스텀 폰트 적용이 끝났다.

UI 요소에 커스텀 폰트를 적용한다(2).
UI 요소에 커스텀 폰트를 적용한다(3).

카테고리 “API/Cocoa”
more...
썸네일 이미지
[iOS・iPadOS] Application의 이름 localization 적용하기.
iOS・iPadOS 다음 게시글: 앱에 커스텀 폰트(custom font) 적용하기. 앱(Application)의 이름 localization 적용하기 iPhone 또는 iPad의 언어 설정에 따라 홈 화면에 나타나는 앱의 이름을 다르게 보여주어야 할 필요가 발생할 수 있다. 예를 들어 카카오톡의 경우도 한국어 상태의 단말기에서는 홈 화면에서 앱 이름이 “카카오톡”과 같이 한글로 나타나지만, 영어 상태의 단말기에서는 앱 이름이 “KakaoTalk”과 같이 영문으로 나타난다. 사용자에게 나타나는 앱의 이름은 CFBundleDisplayName이라는 이름으로 번들(bundle)에 보관되게 된다. 위와 같은 결과를 얻으려면 Xcode에서 다음과 같이 준비한다. 1 단계. 앱이 지원할 수 있는 언어 설정하기 X..
API/Cocoa
2021. 8. 14. 18:14

[iOS・iPadOS] Application의 이름 localization 적용하기.

API/Cocoa
2021. 8. 14. 18:14

iOS・iPadOS

다음 게시글: 앱에 커스텀 폰트(custom font) 적용하기.

 

앱(Application)의 이름 localization 적용하기

iPhone 또는 iPad의 언어 설정에 따라 홈 화면에 나타나는 앱의 이름을 다르게 보여주어야 할 필요가 발생할 수 있다. 예를 들어 카카오톡의 경우도 한국어 상태의 단말기에서는 홈 화면에서 앱 이름이 “카카오톡”과 같이 한글로 나타나지만, 영어 상태의 단말기에서는 앱 이름이 “KakaoTalk”과 같이 영문으로 나타난다. 사용자에게 나타나는 앱의 이름은 CFBundleDisplayName이라는 이름으로 번들(bundle)에 보관되게 된다.

단말기의 언어가 영어로 설정된 상태에서는 앱의 이름이 영어로 표시될 수 있다.
단말기의 언어가 한국어로 설정된 상태에서는 앱의 이름이 한국어로 표시될 수 있다.
단말기의 언어가 프랑스어로 설정된 상태에서는 앱의 이름이 프랑스어로 표시될 수 있다.
단말기의 언어가 러시아어로 설정된 상태에서는 앱의 이름이 러시아어로 표시될 수 있다.
단말기의 언어가 일본어로 설정된 상태에서는 앱의 이름이 일본어로 표시될 수 있다.

 

위와 같은 결과를 얻으려면 Xcode에서 다음과 같이 준비한다.

 

1 단계. 앱이 지원할 수 있는 언어 설정하기

Xcode의 좌측 프로젝트 내비게이터(Project navigator)에서 최상단 항목인 프로젝트를 선택한다. 그 다음 우측 편집 화면에서 프로젝트 이름을 선택한 후 [Info] 탭을 클릭한다.

앱이 지원할 수 있는 언어를 설정하기(1).

 

Localization 항목을 보면 기본으로 영어만 추가되어 있는데, [+] 버튼을 눌러 앱이 지원할 수 있는 언어들을 하나씩 클릭하여 추가시킨다.

앱이 지원할 수 있는 언어를 설정하기(2).

 

언어를 선택할 때마다 프로젝트가 현재 가지고 있는 리소스 파일들의 목록이 나타나며 이러이러한 파일들에 대해 다국어를 지원하겠다는 대화상자가 나타난다. 모두 선택해 주면 좋고, 일부만 선택해서 그것들만 다국어를 지원할 수도 있다. 일단 [Finish]를 눌러본다.

앱이 지원할 수 있는 언어를 설정하기(3).

여기에서는 영어, 한국어, 프랑스어, 러시아어 및 일본어의 5개 국어를 지원해 보겠다.

앱이 지원할 수 있는 언어를 설정하기(4).

앞서 언급된 CFBundleDisplayName라는 이름의 변수는 프로젝트의 info.plist에 정의된다. 그렇다고 info.plist를 열어서 마치 하드코딩처럼 문자열을 막 수정하는 것은 재미없으므로 유연한 방법으로 접근한다.

 

2 단계. info.plist에 대한 localization 적용하기

1 단계에서 앱이 지원할 수 있는 언어들을 결정한 후에 [File] 메뉴에서 새 파일을 추가한다.

localization 파일을 추가한다(1).

새로 추가될 파일의 유형은 strings이다.

localization 파일을 추가한다(2).

 

또한 파일의 이름은 InfoPlist.strings이므로 대화상자에는 InfoPlist를 입력한다. 파일 이름은 바꾸거나 할 수 없고, Xcode에서 항상 그렇게만 쓰겠다고 고정시킨 이름이기에 여기에 따른다.

 

앞서 언급된 CFBundleDisplayName 변수는 이 곳에 놓이게 된다. strings 파일이 추가된 직후에는 대개 영어 모드일 때의 문자열을 지정하게 되므로 다음과 같이 영문 앱 이름을 우선 작성해 본다.

"CFBundleDisplayName"="<영어 앱 이름>";

CFBundleDisplayName는 대소문자가 정확히 일치해야 하며 줄 끝에는 반드시 세미콜론(;)이 붙어야 한다. 그리고나서 화면 우측 메뉴의 [Localize] 버튼을 클릭한다. 앞서 1 단계에서 앱의 언어를 추가할 때마다 이러이러한 리소스 파일에 대해 localization을 적용하겠다고 대화상자가 뜬 것을 보았을 텐데, 이와 같이 해당 대화상자에서는 나타나지 않았던 info.plist와 같은 파일이나 나중에 추가된 리소스 파일 같은 것들은 이러한 방식으로 [Localize] 버튼을 클릭하여 localization을 적용할 수 있다.

기본 앱 이름을 적고 [Localize] 버튼을 클릭(1).

 

현재 편집중인 파일은 영어 모드일 때 사용자에게 보여줄 문자열로 간주하고 영어 리소스에 해당하는 폴더로 이동시키겠다는 대화상자이다. 딱히 수정해야할 것은 없어 보이므로 그냥 [Localize] 누른다.

기본 앱 이름을 적고 [Localize] 버튼을 클릭(2).

 

그러면 [Localize] 버튼이 있던 자리에 1 단계에서 설정했던 언어 목록들이 뜬 것을 볼 수 있다. localize할 언어들을 선택해 준다. 여기에서는 모두 다 선택한다.

기본 앱 이름을 적고 [Localize] 버튼을 클릭(3).

 

언어를 체크할 때마다 프로젝트 내비게이터(Project navigator)에서 InfoPlist.string가 해당 언어 버전으로 분화되는 것을 볼 수 있다. 파일을 하나씩 열어서 그 언어에 맞게 앱 이름을 적어준다. 그러면 단말기의 언어 설정에 따라 앱 이름이 다르게 나타날 것이다.

기본 앱 이름을 적고 [Localize] 버튼을 클릭(4).

 

카테고리 “API/Cocoa”
more...
[단편 Android 개발 정리] 룸(Room) 사용하기 (2021년 버전)
단편 Android 개발 정리 이전 게시글: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전) 룸(Room) 사용하기 (2021년 버전) 안드로이드 앱에서 SQLite는 로컬 데이터베이스로 쓰이고 있다. SQLite를 통해 데이터베이스에 접근하기 위해 이전까지는 DatabaseHelper 클래스를 파생 및 구현시키는 방법을 써 왔으나, 룸(Room)이라는 것이 새로 제공되어 이를 정리해 두고자 한다. 결론부터 말하면 Helper 클래스를 구현하면서 메소드별로, 테이블별로 일일이 커서를 읽고 형변환하고 예외처리하는 등의 귀찮은 작업은 이제 컴파일러가 알아서 도맡을테니 메타 데이터로 껍데기만 알려주면 되는 편리한 기능이다. 컴파일러 의존성 설정하기 2021년 기준 최신 Android Stud..
API/Android
2021. 7. 8. 22:32

[단편 Android 개발 정리] 룸(Room) 사용하기 (2021년 버전)

API/Android
2021. 7. 8. 22:32

단편 Android 개발 정리

이전 게시글: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

 

룸(Room) 사용하기 (2021년 버전)

안드로이드 앱에서 SQLite는 로컬 데이터베이스로 쓰이고 있다. SQLite를 통해 데이터베이스에 접근하기 위해 이전까지는 DatabaseHelper 클래스를 파생 및 구현시키는 방법을 써 왔으나, 룸(Room)이라는 것이 새로 제공되어 이를 정리해 두고자 한다. 결론부터 말하면 Helper 클래스를 구현하면서 메소드별로, 테이블별로 일일이 커서를 읽고 형변환하고 예외처리하는 등의 귀찮은 작업은 이제 컴파일러가 알아서 도맡을테니 메타 데이터로 껍데기만 알려주면 되는 편리한 기능이다.

 

컴파일러 의존성 설정하기

2021년 기준 최신 Android Studio에서 Room을 사용하고자 한다면 다음과 같이 gradle에 의존성을 추가하면 된다. 이 역시 나중에 Android Studio가 버전 상승하면 할 필요가 없어지려나?

dependencies {
// ... 기본적으로 생성되는 코드들은 생략하고 끝에 이런 내용들을 추가한다.

// 2021년 현재 2.3.0이 최신 버전이나 2.2.5를 써 보겠다.
def $room_version = "2.2.5"

// 클래스나 매개변수 앞에 붙는 annotation을 처리하기 위함
implementation 'androidx.room:room-runtime:$room_version'
annotationProcessor 'androidx.room:room-compiler:$room_version'

// 아래 내용이 없으면 나중에 실행할 때
// 클래스명_Impl does not exists라는 오류가 발생함
apply plugin: 'kotlin-kapt'
kapt 'androidx.room:room-compiler:$room_version'

물론 다음과 같이 버전을 하드코딩해도 된다. 나중에 룸의 버전이 올라갔을 때 불편할 수는 있겠다.

dependencies {
// ... 기본적으로 생성되는 코드들은 생략하고 끝에 이런 내용들을 추가한다.

apply plugin: 'kotlin-kapt'
kapt 'androidx.room:room-compiler:2.2.5'

implementation 'androidx.room:room-runtime:2.2.5'
annotationProcessor 'androidx.room:room-compiler:2.2.5'

kapt라는 것이 등장하는데, 이것은 kotlin 컴파일러에 부착되어서 돌아가는 플러그인 같은 것이다.

 

엔티티를 나타내는 클래스 선언하기

엔티티(entity), 행(row), 레코드(record)... 라고 불리는 것을 클래스 형태로 선언할 차례다. 예를 들어 SQL로 다음과 같이 생성된 테이블에 접근하고자 한다.

CREATE TABLE "Entity" (
	"id"	INTEGER NOT NULL UNIQUE,
	"title"	TEXT NOT NULL,
	"content"	TEXT,
	"data"	BLOB,
	PRIMARY KEY("id")
);
CREATE INDEX "IndexEntity" ON "Entity" (
	"id"	ASC
);

id는 정수형 값이고 NULL을 허용하지 않으며 테이블 내에서 유일해야 하고 인덱스까지 되어 있다고 가정한다. titleNULL을 허용하지 않는 문자열이다. content도 문자열이지만 NULL을 허용한다. data는 이진(binary) 데이터이다.

이 테이블에 보관되는 각 행을 표현하는 kotlin 클래스는 다음과 같이 적을 수 있다.

@Entity(tableName = "Entity", primaryKeys = ["id"], indicies = [Index(name = "IndexEntity", value = "id")]
class Entity(@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER) 
             var id: Int, 
             @ColumnInfo(name = "title", typeAffinity = ColumnInfo.TEXT)
             var title: String,
             @ColumnInfo(name = "content", typeAffinity = ColumnInfo.TEXT)
             var content: String?,
             @ColumnInfo(name = "data", typeAffinity = ColumnInfo.BLOB)
             var data: ByteArray?) {
	// ...
}

titlecontent의 차이에 주목한다. 하나는 NULL을 허용하지 않고 다른 하나는 허용한다. 이 차이는 annotation에서 드러나지 않는다. 둘 다 타입이 ColumnInfo.TEXT로 되어 있기 때문이다. 대신 매개변수의 형식을 본다. 하나는 String이고 다른 하나는 nullable인 String?이다.

SQLite에서 BLOB으로 선언한 필드의 경우 kotlin과 연계 시 그 자료형은 ByteArray로 될 수 있다.

테이블을 생성할 때 사용한 SQL 구문의 내용과 코틀린 클래스의 annotation으로 지정한 내용에 차이가 있을 경우 런타임 시 테이블 선언이 서로 다르다는 오류를 뱉을 수 있다.

 

엔티티에 대해 할 수 있는 조회, 삽입 및 삭제 작업 선언하기

이전까지는 Helper 클래스를 작성하면서 이를 직접 구현하였다. SQL 구문을 구성해서 쿼리 수행하고, 결과를 읽어야 한다면 커서(Cursor)를 열고 필드 순번으로써 해당 필드를 접근하고 예외 발생 시 오류처리 등등... 이것이 룸(Room)을 사용하면 단순히 인터페이스 선언만으로 축약된다. 인터페이스를 구현하는 작업은 앞서 플러그인의 버프를 받는 컴파일러가 수행한다. 즉, 인터페이스에 대한 클래스 구현은 컴파일러가 알아서 해 준다. 이와 같이 테이블의 행들을 가지고 작업할 수 있는 객체를 DAO(Database Access Object)라 한다.

@Dao
interface IEntityAccess {
	@Query("SELECT * FROM Entity")
    fun select(): List<Entity>
    @Query("SELECT * FROM Entity WHERE title LIKE :condition")
    fun select(condition: String?): List<Entity>
}

이런 식으로 쿼리를 작성한다. 메소드의 반환형은 대체로 리스트면 무난할 것 같다. 매개변수는 쿼리 안에서 콜론을 붙이고 매개변수 이름을 그대로 적으면 된다.

UPDATE, INSERT, UPDATE 등의 코드는 annotation만 작성하면 된다. 쿼리는 알아서 작성해준다. 이 때 기본 키를 가지고 특정 행을 식별하는데, 기본키에 해당하는 필드 자체를 수정할 수는 없다. 기본키 필드에 담긴 값 자체를 수정하려면 위와 같이 쿼리를 직접 작성해야 한다.

@Dao
interface IEntry2Access {
	@Insert
    fun insert(entry: Entry2)
    @Update
    fun update(entry: Entry2) // 수정할 행의 기본 키와 새로 수정할 내용이 담긴 객체
    @Delete
    fun delete(entry: Entry2)
}

 

데이터베이스 클래스 작성하기

엔티티를 정의했고, 그 엔티티에 대해 수행하는 작업을 정의했다. 그러한 엔티티들을 포함하는 단위인 데이터베이스도 클래스로 작성할 차례이다.

@Database(entities = [Entity::class, Entity1::class, Entity2::class], version = 1, exportSchema = false)
abstract class Database : RoomDatabase() {
    abstract fun accessEntity(): IEntityAccess
    abstract fun accessEntity1(): IEntityAccess
    companion object {
    	private val instance: Database? = null
    	@Synchronized
        fun getInstance(context: Context): Database {
        	if (instance == null) {
            	synchronized(Database::class) {
                    instance = Room.databaseBuilder(
                        context,
                        Database::class.java,
                        파일경로).build()
                }
            }
            
            return instance!!
        }
    }
}

asset에 이미 구촉된 데이터베이스가 있다면 이것을 앱 로컬 경로로 복사하는 식으로 생성한다.

instance = Room.databaseBuilder(
	context,
	Database::class.java,
	파일경로).createFromAsset(이름).build()

 

카테고리 “API/Android”
more...
썸네일 이미지
[단편 Android 개발 정리] 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)
단편 Android 개발 정리 이전 게시글: ActionBar 대신 Toolbar로 대체하기(2021년 버전) 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전) 이전 게시글에서 ActionBar를 Toolbar로 대체한 다음 햄버거 버튼까지 추가해 보았다. 본 게시글에서는 햄버거 버튼을 클릭하면 사이드 메뉴가 나타나는 것까지 구현해 보겠다. 안드로이드에서는 햄버거 버튼을 클릭했을 때 측면에서 스르륵 나타나는 사이드 메뉴를 DrawerLayout이라고 부른다. 이전 게시글의 레이아웃에서 이를 덧붙여보겠다. 이전 게시글에서 햄버거 버튼까지 반영했을 때의 레이아웃은 다음과 같다. 그리고 액티비티에 대한 코틀린 클래스는 다음과 같다. import는 생략한다. class MainActivity :..
API/Android
2021. 7. 8. 21:22

[단편 Android 개발 정리] 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

API/Android
2021. 7. 8. 21:22

단편 Android 개발 정리

이전 게시글: ActionBar 대신 Toolbar로 대체하기(2021년 버전)

 

사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

이전 게시글에서 ActionBar를 Toolbar로 대체한 다음 햄버거 버튼까지 추가해 보았다. 본 게시글에서는 햄버거 버튼을 클릭하면 사이드 메뉴가 나타나는 것까지 구현해 보겠다. 안드로이드에서는 햄버거 버튼을 클릭했을 때 측면에서 스르륵 나타나는 사이드 메뉴를 DrawerLayout이라고 부른다. 이전 게시글의 레이아웃에서 이를 덧붙여보겠다.

이전 게시글에서 햄버거 버튼까지 반영했을 때의 레이아웃은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:menu="@menu/menu_toolbar">
    </androidx.appcompat.widget.Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>

그리고 액티비티에 대한 코틀린 클래스는 다음과 같다. import는 생략한다.

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		val toolbar = findViewById<Toolbar>(R.id.toolbar)
		setSupportActionBar(toolbar)

		supportActionBar?.setDisplayHomeAsUpEnabled(true)
		supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
	}

	override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    	// ...
		return super.onCreateOptionsMenu(menu)
	}

	override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
    	// ...
		return super.onPrepareOptionsMenu(menu)
	}

	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
        	// ...
		}

		return result;
	}
}

ConstraintLayout을 기준으로 설명하면, 맨 위에 Toolbar가 있고 그 아래에 붙여서 DrawerLayout을 배치할 것이다. 그러면 레이아웃은 다음과 같이 작성할 수 있다.

<androidx.constraintlayout.widget.ConstraintLayout ...>
	<androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" ...>
	</androidx.appcompat.widget.Toolbar>
	<androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/layout_drawer"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="이 곳은 메인 영역이다." />
        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navigation_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:fitsSystemWindows="true">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="이 곳은 사이드 영역이다." />
        </com.google.android.material.navigation.NavigationView>
    </androidx.drawerlayout.widget.DrawerLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

TextView는 영역 사이의 구분을 명확히 하기 위하여 임의로 넣은 요소이다. 아무튼 레이아웃을 저렇게 작성한다. DrawerLayout 내부에 사이드 메뉴가 보여질 NavigationView가 있다는 것이다. 특히 주목할 것은 NavigationView가 갖는 android:layout_gravity="start" 속성이다. 이 속성은 사이드 메뉴가 왼쪽에서 나타났다가 왼쪽으로 사라짐을 의미한다. 오른쪽 사이드 메뉴는 android:layout_gravity="end"로 하면 된다.

그 다음 이전 포스트에서 햄버거 메뉴를 클릭할 때 호출되는 이벤트는 다음과 같이 처리한다. 사이드 메뉴가 이미 떠 있다면(isDrawerOpen) 그것을 감추고(closeDrawer), 사이드 메뉴가 감춰져 있다면 그것을 드러낸다(openDrawer).

class MainActivity : AppCompatActivity() {
	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
			android.R.id.home -> {
				val layoutDrawer = findViewById<DrawerLayout>(R.id.layout_drawer)
				val gravity = GravityCompat.START

				if (layoutDrawer.isDrawerOpen(gravity)) {
					layoutDrawer.closeDrawer(gravity)
				} else {
					layoutDrawer.openDrawer(gravity)
				}
			}
		}

		return result;
	}
}

실행 결과는 다음과 같다.

햄버거 버튼을 누르기 전
햄버거 버튼을 누른 후

디자인적 요소는 최대한 배제한 채, 최소한 접고 펼치는 기능적인 구현은 이것으로 끝.

 

카테고리 “API/Android”
more...
썸네일 이미지
[단편 Android 개발 정리] ActionBar 대신 Toolbar로 대체하기(2021년 버전)
단편 Android 개발 정리 다음 포스트: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전) ActionBar 대신 Toolbar로 대체하기 2021년 기준 최신 버전의 Android Studio에서 앱의 ActionBar 대신 Toolbar로 대체하는 방법을 단계별로 정리해보겠다. 1 단계. Empty Project 생성하기 Android Studio를 열고 아무 액티비티도 갖지 않는 빈 프로젝트를 하나 생성한다. 프로젝트의 이름와 도메인은 적절하게 입력한다. 그 다음 /res/values/themes/themes.xml를 열면 style 태그가 ActionBar로부터 파생되었음을 지정하는 내용이 있을 것이다. 일단 확인만 하고 놔 둔다. 2 단계. 빈 액티비티 추가하기 빈 액티비티를..
API/Android
2021. 7. 7. 13:39

[단편 Android 개발 정리] ActionBar 대신 Toolbar로 대체하기(2021년 버전)

API/Android
2021. 7. 7. 13:39

단편 Android 개발 정리

다음 포스트: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

 

ActionBar 대신 Toolbar로 대체하기

2021년 기준 최신 버전의 Android Studio에서 앱의 ActionBar 대신 Toolbar로 대체하는 방법을 단계별로 정리해보겠다.

 

1 단계. Empty Project 생성하기

Android Studio를 열고 아무 액티비티도 갖지 않는 빈 프로젝트를 하나 생성한다. 프로젝트의 이름와 도메인은 적절하게 입력한다.

아무것도 Activity도 포함하지 않는 새 프로젝트를 선택한다.

그 다음 /res/values/themes/themes.xml를 열면 style 태그가 ActionBar로부터 파생되었음을 지정하는 내용이 있을 것이다. 일단 확인만 하고 놔 둔다.

기본 스타일이 ActionBar로부터 파생되고 있다.

 

2 단계. 빈 액티비티 추가하기

빈 액티비티를 하나 추가한다. 이름은 적절히 부여한다.

activity_main.xml

아무것도 수정하지 않은 맨 처음 상태의 빈 액티비티라면 이렇게 생겼을 것이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	tools:context=".MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.kt

또한 비하인드 코드는 이렇게 생겼다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)
	}
}

 

3 단계. ActionBar를 지우기

2 단계에서 아무것도 건드리지 않고 그냥 실행을 해 보겠다. 그러면 다음과 같이 상단에 ActionBar를 포함하는 빈 화면이 나타난다.

기본 설정에 따라 ActionBar가 추가되어 있는 빈 액티비티

 

여기에서부터 하나씩 수정해나가겠다. 앞서 확인했던 /res/values/themes/themes.xml 파일을 다시 열고 앱이 정의하고 있는 기본 테마를 ActionBar에서 NoActionBar 계열로 바꾼다. 그러면 AndroidManifest.xmlapplication에서 themes.xml에서 정의된 테마를 참조하고 그 결과가 application의 일부인 MainActivity에도 반영된다.

NoActionBar 테마를 적용한다.

정리하자면,

themes.xmlTheme.MaterialComponents.DayNight.NoActionBar로부터 파생된 Theme.Testapp라는 이름의 새 테마를 정의한다.

AndroidManifest.xmlTheme.Testapp를 참조하여 앱의 전반적인 테마를 적용한다.

MainActivity도 여기에 영향을 받아 ActionBar가 사라진다.

NoActionBar가 적용된 앱의 모습

 

4 단계. ActionBar가 사라진 자리에 Toolbar 넣기

이전 버전의 Android Studio에서는 Toolbar의 이름이 다음과 같이 android.support.v7.widget.Toolbar였다.

<android.support.v7.widget.Toolbar
	android:id="..." />

그러나 2021년 현재 최신 Android Studio에는 이 코드가 오류가 난다. Toolbar의 이름은 다음과 같이 바뀌었다. 물론 gradle에서 의존성을 수정하고 번거로운 세팅을 하면 옛날 이름으로 쓸 수 있겠으나, 이제부터 개발하는 앱은 기본 설정을 사용하는 것으로...

<androidx.appcompat.widget.Toolbar
	android:id="..." />

다시 나타난 것은 ActionBar가 아니라 Toolbar임

이제 MainActivty의 비하인드 코드 측에도 레이아웃에 새로 추가된 Toolbar를 ActionBar처럼 사용하라고 지정해 주어야 한다. MainActivty.kt를 열고 onCreate 메서드의 맨 끝 부분에 다음의 내용을 적는다.

val toolbar = findViewById<Toolbar>(R.id.toolbar)
//setSupportActionBar(toolbar)

onCreate 메소드의 전체적인 내용은 다음과 같아야 할 것이다. super.onCreate를 호출하고 setContentView를 실행한 다음에 새로 작성하는 문장들이 등장해야 할 것이다.

override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	setContentView(R.layout.activity_main)

	val toolbar = findViewById<Toolbar>(R.id.toolbar)
	setSupportActionBar(toolbar)
}

그러면 ActionBar처럼 Toolbar에도 앱의 이름이 나타나는 것을 볼 수 있다. 텍스트의 색이 조금 달라 보이는 것은 기분 탓일 것이다.

완성된 Toolbar의 모습

 

5 단계. 메뉴 추가하기

ActionBar와 달리 Toolbar는 View의 일종이기 때문에 버튼이나 메뉴 등 다양한 요소들의 기능과 위치를 제어하기가 편리하다.

 

메뉴 작성하기

프로젝트에 /res/menu 디렉토리를 하나 생성한다. 여기에 메뉴를 정의하는 XML 파일을 생성한다. 이 파일에 Toolbar에 넣을 메뉴들을 적는다.

<?xml version="1.0" encoding="utf-8"?>
<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_item0"
        android:title="바나나"
        app:showAsAction="ifRoom" />
    <item android:id="@+id/action_item1"
        android:title="포도"
        app:showAsAction="never" />
    <item android:id="@+id/action_item2"
        android:title="딸기"
        app:showAsAction="never" />
    <item android:id="@+id/action_item3"
        android:title="포도"
        app:showAsAction="never" />
</menu>

여기서 app:showAsAction 속성은 메뉴 항목을 Toolbar 우측 [더 보기...] 버튼을 눌러야 나타나게 할 것인지 여부를 지정한다.

  • never는 해당 항목을 무조건 [더 보기...] 버튼을 눌러야만 나타나게 한다.
  • ifRoom은 화면의 폭이 충분할 때는 Toolbar에 직접 나타내고 좁을 때는 [더 보기...] 버튼을 통해 보여지게 한다.
  • always라고 해서 화면의 폭과 무관하게 무조건 Toolbar에 직접 나타내는 옵션이 있는데 이는 IDE에서 권장하지 않고 ifRoom을 사용하도록 유도하고 있다.

설명보다는 직접 보고 정리하면 되겠다.

메뉴를 정의하는 xml 파일

 

레이아웃에 메뉴 적용하기

레이아웃 파일의 androidx.appcompat.widget.Toolbar로 돌아가서 다음의 속성을 추가하면 Toolbar에 메뉴가 적용된다.

app:menu="@menu/파일.xml"

"바나나" 항목은 app:showAsAction="ifRoom"으로 지정했으므로 Toolbar에 바로 나타난다.

ifRoom 값을 지정한 메뉴 항목

app:showAsAction="never"로 지정한 항목은 [더 보기...] 버튼을 눌러야 나타난다.

never 값을 지정한 메뉴 항목

 

6 단계. 메뉴 항목마다 이벤트 작성하기

Toolbar와 menu의 겉 모양은 대충 만들었으니 비하인드 코드를 작성할 차례이다. kotlin 소스 코드를 열고 액티비티에 다음과 같이 메소드를 오버라이드한다.

class MainActivity: AppCompatActivity() {
	// ...
	override fun onCreateOptionsMenu(menu: Menu?): Boolean {
		Log.d("menu", "onCreateOptionsMenu")
		menuInflater.inflate(R.menu.menu_toolbar, menu)
		return super.onCreateOptionsMenu(menu)
	}

	override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
		Log.d("menu", "onPrepareOptionsMenu")
		return super.onPrepareOptionsMenu(menu)
	}

	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
			R.id.action_item0 -> { Log.d("menu", "바나나 선택함"); result = true }
			R.id.action_item1 -> { Log.d("menu", "포도 선택함"); result = true }
			R.id.action_item2 -> { Log.d("menu", "딸기 선택함"); result = true }
		}

		return result;
	}
}

각 메소드가 언제 실행되는지 Logcat으로 찍어보면 알 수 있다

onCreateOptionsMenu는 액티비티에 딸려 있는 메뉴를 최초로 구성할 때 1회만 호출된다. 이것을 재정의하여 menuInflater.inflate를 호출해준다. menuInflater.inflate 메소드가 무엇인가 하니, 리소스로부터 메뉴 XML 파일을 불러와서 메뉴로 띄워주는 역할을 한다. 레이아웃에서 지정했던 app:menu="@menu/파일.xml"과 동일한 것이다. 다만 레이아웃에서 app:menu="@menu/파일.xml"을 굳이 적지 않고 menuInflater.inflate 메소드만 호출해도 앱 실행 시 메뉴가 나타났지만, 반대로 menuInflater.inflate 메소드를 호출하지 않고 레이아웃에게 app:menu="@menu/파일.xml"만을 지정하면 메뉴가 나타나지 않았다. 그냥 그렇다는 거다.

onPrepareOptionsMenu는 메뉴를 최초로 구성할 때는 물론이고 [더 보기...] 버튼을 클릭하여 메뉴가 펼쳐질 때마다 호출된다. 사용자에게 메뉴 항목을 보여주기 전 뭔가를 메뉴 항목을 업데이트할 때 유용할 것으로 보인다.

onOptionsItemSelected는 메뉴 항목을 클릭/터치할 때 호출된다. 어떤 메뉴가 클릭/터치되었는지는 매개변수로 넘어온 item: MenuItemmenu.itemId를 통해 식별 가능하다.

 

7 단계. 햄버거 버튼 만들기

이렇게까지 ActionBar를 굳이 ToolBar로 바꾸어서 버튼들을 추가하는 이유 중 하나는 사이드 바 메뉴, 일명 "햄버거 버튼(hamburger button)"을 만들기 위함일 것이다. 햄버거 버튼을 추가해 보겠다.

 

햄버거 이미지 준비하기

먼저 햄버거 아이콘을 준비한다. 직접 만들 필요도 없고, 구글링 통해 굳이 직접 구할 필요도 없다. Android Studio의 클립아트에 미리 준비되어 있다. drawable 디렉토리에 새로운 Vector(Image) Asset을 추가해본다.

New 메뉴에서 Vector(Image) Asset 항목 클릭

여기서는 벡터 이미지를 넣어보겠다. 클립아트 옆의 그림을 클릭하면 미리 준비된 아이콘들의 목록이 나타난다. 이 중에서 햄버거 아이콘(이름: menu)을 찾아서 클릭하고, 색상과 크기(일단은 넉넉하게 크기를 지정해 준다)를 적절하게 넣어주면 끝.

표시된 부분을 클릭
menu라고 이름이 붙은 아이콘 선택

그리고 onCreate 메소드로 가서 홈(Home) 버튼이 Toolbar에 생겨나도록 다음과 같이 문장을 작성한다. 앞서 만들어 본 메뉴 항목과 달리 햄버거 아이콘은 대체로 화면의 왼쪽에 위치할텐데, Toolbar의 왼쪽에 생기는 아이콘을 홈 버튼이라 한다.

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		val toolbar = findViewById<Toolbar>(R.id.toolbar)
		setSupportActionBar(toolbar)

		// 새로 추가된 두 줄
		supportActionBar?.setDisplayHomeAsUpEnabled(true)
		supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
	}
}

setDisplayHomeAsUpEnabled은 매개변수에 true를 전달함으로써 Toolbar에 홈 버튼이 나타나게 한다. setHomeAsUpIndicator은 홈 버튼으로 표시할 아이콘을 지정하는데, 위에서 클립아트를 통해 추가한 햄버거 아이콘의 아이디를 적으면 된다.

그러면 이와 같이 넉넉한 사이즈로 햄버거 아이콘이 나타난다

이런 모양을 원했던 것이 아니다. 그렇다면 화면 크기에 맞게 이미지 크기를 축소해주어야 한다. 여기서는 벡터 형식으로 아이콘을 추가했으므로 크기 조정이 매우 간편하다. 햄버거 아이콘 파일을 열고 폭(android:width)과 높이(android:height)를 ?attr/actionBarSize로 수정한다. 그러면 다음과 같이 제법 그럴듯한 아이콘이 만들어진다.

<vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="?attr/actionBarSize"
    android:tint="#FFFFFF"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="?attr/actionBarSize">
    <path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>

한결 그럴듯한 햄버거 아이콘

이것도 크다 하면 마진(margin)을 좀 더 주는 식으로 줄일 수 있다. path element를 group으로 감싼 다음 scaleXscaleY1.0보다 작은 값으로 지정한다. 그러면 path가 표현하는 햄버거 아이콘이 좀 더 축소된다. 예를 들어 scaleX="0.8" scaleY="0.8"로 하면 가로와 세로를 본래의 80%로 축소시킨다.

좀 더 보기 좋아졌으나 뭔가 아쉬운 아이콘

크기는 좀 줄었는데 위치가 애매해졌다. 아이콘을 버튼 영역의 정중앙에 오도록 하려면 pivotXpivotY를 각각 android:viewportWidthM의 절반, android:viewportHeight의 절반으로 지정하면 된다.

<vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="?attr/actionBarSize"
    android:tint="#FFFFFF"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="?attr/actionBarSize">
    <group
        android:scaleX="0.8"
        android:scaleY="0.8"
        android:pivotX="12"
        android:pivotY="12">
        <path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
    </group>
</vector>

딱 좋은 상태의 햄버거 아이콘

이 버튼을 터치했을 때의 이벤트 처리는 다음과 같이 작성한다.

class MainActivity : AppCompatActivity() {
	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
			android.R.id.home -> { Log.d("menu", "햄버거 버튼"); result = true }
            // ...
		}

		return result;
	}
}

햄버거 버튼, 홈 버튼의 ID는 android.R.id.home으로 고정되어 있다.

카테고리 “API/Android”
more...

“분류 전체보기” (134건)