전체 페이지뷰

2017년 2월 1일 수요일

Chapter 2. Anatomy of an app, part 2

PCL or SAP?

처음 Hello 솔루션을 생성할 때 두 개의 application template 중 선택해야 했습니다.
  • Blank App (Xamarin.Forms Portable)
  • Blank App (Xamarin.Forms Shared)
Xamarin Studio에서는 한 쌍의 radio 버튼 가운데서 선택했습니다.
  • Use Portable Class Library
  • Use Shared Library
첫 번째 옵션은 PCL (Portable Class Library)을 만들고 두 번째 옵션은 공유 코드 파일로만 구성된 SAP (Shared Asset Project)를 만듭니다. 원래의 Hello 솔루션은 PCL 템플릿을 사용했습니다. 이제 SAP 템플릿을 사용하여 HelloSap이라는 두 번째 솔루션을 만듭니다.
(비주얼 스튜디오의  파일>새로 만들기>프로젝트, 새 프로젝트 창에서 템플릿>Visual C#>Cross-platform> Blank App (Xamarin.Forms Shared) )

HelloSap 프로젝트에는 하나의 항목(App.cs)만 포함된다는 점을 제외하면 모든 것이 거의 동일합니다.

PCL, SAP 어느 쪽이던 다섯 프로젝트 간에 코드가 공유되지만, 결정적으로 다른 점이 있습니다. PCL 방식에서는 모든 공통 코드가 하나의 dll(dynamic-link library)로 묶어지고, 런타임에 각각의 프로젝트가 그것을 참조하고, 바인딩합니다. SAP 방식에서는 공통 코드가 빌드 타임에 각각의 프로젝트에 포함되게 됩니다. 기본적으로 SAP에 존재하는 App.cs는 마치 5개의 다른 프로젝트가 있는 것과 같은 효과를 보입니다.

약간 미묘한 문제가 shared library 방식에서 나타나는데,
iOS 및 Android 프로젝트는 동일한 버전의 .NET에 액세스 할 수 있지만, Windows 프로젝트에서 사용하는 것과 동일한 버전의 .NET이 아니라는 점입니다. 그것이 의미하는 것은 공유 코드가 액세스하는 모든 .NET 클래스가 플랫폼에 따라 다소 다를 수 있다는 것입니다. 이 책의 후반에 나오겠지만, System.IO 네임스페이스의 일부 file I/O 클래스의 경우가 거기에 속합니다.

C# 전처리기 지시문, 특히 #if 및 #elif를 사용하여 이러한 차이점을 보완합니다. Xamarin.Forms 템플릿에 의해 생성된 프로젝트에서, 다양한 응용 프로그램 프로젝트들이 이런 기호를 정의합니다.

이 기호들은 무엇일까요?

Visual Studio에서 솔루션 탐색기에서 프로젝트 이름들을 마우스 우클릭하고 속성을 선택합니다. 속성 화면 왼쪽에서 빌드를 선택하고 조건부 컴파일 기호 필드를 찾습니다.

Xamarin Studio에서는 Solution 목록의 응용 프로그램 프로젝트를 선택하고, 드롭 다운 도구 메뉴를 활성화 한 다음 Options을 선택합니다. Project Options 대화 상자의 왼쪽에서 Build>Compilers를 선택하고 Define Symbols 필드를 찾으십시오

사용 가능한 심볼들은 다음과 같습니다.
  • iOS 프로젝트 : __IOS__  (앞 뒤로 언더스코어 두개)
  • Android 프로젝트 : 아무것도 보이지 않습니다만  __ANDROID__ 식별자가 정의 가능합니다. __ANDROID_nn__ 식별자도 가능합니다(여기서 nn은 지원되는 각 Android API 레벨입니다).
  • UWP 프로젝트 :  WINDOWS_UWP
  • Windows 프로젝트 : WINDOWS_APP
  • Windows Phone 프로젝트 : WINDOWS_PHONE_APP
따라서 shared code 파일은 다음과 같은 코드 블럭을 사용할 수 있습니다.

#if __IOS__ 
    // iOS specific code 
#elif __ANDROID__ 
    // Android specific code 
#elif WINDOWS_UWP 
    // Universal Windows Platform specific code 
#elif WINDOWS_APP 
    // Windows 8.1 specific code 
#elif WINDOWS__PHONE_APP 
    // Windows Phone 8.1 specific code #endif
cs

이를 통해 공유 코드 파일은 플랫폼 별 코드를 실행할 수도 있고, 개별 플랫폼 프로젝트의 클래스를 포함하여 플랫폼별 클래스에 액세스 할 수 있습니다. 원하는 경우 사용자 고유의 조건부 컴파일 기호를 정의 할 수도 있습니다.

이러한 전 처리기 지시문은 PCL 프로젝트에서 의미가 없습니다. PCL은 5 개의 플랫폼과 완전히 독립적이며, PCL이 컴파일 될 때 플랫폼 프로젝트 식별자는 더 이상 존재하지 않게 됩니다.

이런 PCL 개념은 원래 .NET을 사용하는 플랫폼들이 서로간에 다소 다른 하위 버전을 사용하기 때문에 생겨났습니다. 여러 .NET 플랫폼에서 사용할 수있는 라이브러리를 만들려면 해당 .NET 버전의 공통부분만 사용해야합니다.

여러 .NET 플랫폼에서 사용할 수 있는 코드를 포함하고 있기 때문에 PCL이 도움이 됩니다. 따라서 특정 PCL에는 지원되는 플랫폼을 나타내는 내장 플래그가 포함되어 있습니다. Xamarin.Forms 응용 프로그램에 사용되는 PCL은 다음 플랫폼을 지원해야합니다.


  • .NET Framework 4.5
  • Windows 8
  • Windows Phone 8.1
  • Xamarin.Android
  • Xamarin.iOS
  • Xamarin.iOS (Classic)

이것들을 통틀어 PCL Profile 111 이라 합니다.

PCL에서 플랫폼 특정 동작이 필요한 경우에는 빌드 타임에만 작동하는 C # 전처리기 지시문을 사용할 수 없습니다. Xamarin.Forms Device 클래스와 같이 런타임에 작동하는 무언가가 필요합니다. 곧 예제를 보여 드릴 것입니다.

Xamarin.Forms PCL은 동일한 플랫폼을 지원하는 다른 PCL에 액세스 할 수 있지만 개별 응용 프로그램 프로젝트에 정의된 클래스에 직접 액세스 할 수는 없습니다. 그러나 그렇게 하길 원하신다면 Chapter.9, "Platform-specific API calls"에서 DependencyService라는 클래스를 이용하여 PCL을 가지고서 접근하는 예제를 찾아볼 수 있습니다.

이 책에서는 현재 Xamarin.Forms를 가지고 작업하는 프로그래머들이 좀 더 선호하는 방식인 PCL을 주로 사용합니다. 그러나, SAP 방식을 선호하는 사람들도 있으며 장점도 있습니다. 이제부터 SAP 방식으로 된 프로그램을 보여드릴 때는 HelloSap 프로그램처럼 Sap이라는 어미를 붙이겠습니다.

굳이 둘 중에 선택하지 않고서 하나의 솔루션 내에 둘 다를 가질 수도 있습니다. 만약 이미 생성한 SAP 프로젝트 내에서, 새 프로젝트를 추가하여 Class Library (Xamarin.Forms Portable)를 선택하면 SAP과 PCL에 모두 access할 수 있게 됩니다.

Labels for text

Hello 솔루션을 생성할 때와 같은 방법으로 Greetings라는 새 Xamarin.Forms PCL 프로젝트를 생성하십시오.

Visual Studio에서 솔루션 탐색기에서 Greetings 프로젝트를 마우스 오른쪽 단추로 클릭하고 메뉴에서 추가> 새 항목을 선택할 수 있습니다. 새 항목 추가 대화 상자의 왼쪽에서 Visual C# Cross-Platform을 선택하고 가운데 영역에서 Forms Page를 선택합니다. (주의 : Forms View가 아닙니다). 그리고 이름을 GreetingsPage.cs 로 지정합니다.

Xamarin Studio에서는 Greetings 프로젝트의 tool icon중, Add> New File을 선택합니다. New File 창의 좌측에서 Forms를 선택하고, 가운데에서 Forms ContentPage를 고릅니다(Forms ContentViewForms ContentPage Xaml이 아닙니다). 역시 GreetingsPage.cs 로 이름을 지정합니다.

GreetingsPage.cs 파일은 ContentPage로부터 파생된 GreetingsPage라는 클래스의 뼈대 코드로 초기화될 것입니다. ContentPageXamarin.Forms 네임스페이스에 속해 있으므로 using 지시문에 들어 있습니다. 이 클래스는 외부로부터 직접 접근될 것이 아니므로 그럴 필요는 없지만 public으로 설정되어 있습니다.

GreetingsPage 생성자와 using 지시문 몇개를 지우고 나서 보면 코드는 다음과 같을 것입니다.
using System;
using Xamarin.Forms;
namespace Greetings
{
    public class GreetingsPage : ContentPage
    {
        public GreetingsPage()
        {
            
        }
    }
}
cs

GreetingsPage 클래스의 생성자에서 Label 뷰를 인스턴스화하고, Text property를 설정한 다음, 해당 Label 인스턴스를 GreetingsPageContentPage로부터 상속받는 Content 속성으로 설정합니다. 
using System;
using Xamarin.Forms;
namespace Greetings
{
    public class GreetingsPage : ContentPage
    {
        public GreetingsPage()
        {
            Label label = new Label();
            label.Text = "Greetings Xamarin.Forms!";
            this.Content = label;
        }
    }
}
cs

다음으로 App.cs의 App 클래스를 변경하여 MainPage 프로퍼티를 이 GreetingsPage 클래스의 인스턴스로 설정합니다.
using System;
using Xamarin.Forms;
namespace Greetings
{
    public class App : Application
    {
        public App()
        {
            MainPage = new GreetingsPage();
        }
        protected override void OnStart()
        {
            // Handle when your app starts
        }
        protected override void OnSleep()
        {
            // Handle when your app sleeps
        }
        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}
cs
이 단계를 잊어버리면 프로그램이 여전히 "Welcome to Xamarin.Forms!"라고 출력되어 당황하실 겁니다.

자마린 공부의 초반부에는 이 GreetingsPage 클래스 내에서 대부분의 시간을 보내시게 될겁니다. single-page UI 집중 형 프로그램의 경우,이 클래스에는 필요한 코드만 포함될 수 있고, 물론 필요한 경우 프로젝트에 클래스를 추가 할 수도 있습니다.

대부분의 single-page 앱의 경우, ContentPage에서 파생한 클래스 이름은 솔루션이름+Page(GreetingPage 처럼)로 명명합니다. 이 명명 규칙은 전체 파일을 보지 않고도 클래스 또는 생성자 이름만으로 코드 목록을 식별하는 데 도움이 됩니다. 대부분의 경우 이 책의 페이지에 있는 코드 snippet은 using 지시문이나 namespace 정의를 포함하지 않는다는 것도 알아두시길 바랍니다.

많은 Xamarin.Forms 프로그래머는 페이지 생성자의 객체 생성 및 프로퍼티 초기화에 있어 C# 3.0 스타일을 선호합니다. Label 객체 생성에서 그것을 따라해볼 수 있습니다. Label 생성과 함께 중괄호 안에 콤마로 구분된 프로퍼티들을 넣고 세미콜론을 붙이는 방법입니다.
다음은 위에서 한 GreetingsPage 정의와 문법만 다르고 기능적으로 완전히 같은 구문입니다.
using System;
using Xamarin.Forms;
namespace Greetings
{
    public class GreetingsPage : ContentPage
    {
        public GreetingsPage()
        {
            Label label = new Label {
                Text = "Greetings Xamarin.Forms!"
            };            
            this.Content = label;
        }
    }
}
cs

이 스타일의 속성 초기화를 사용하면 Label 인스턴스가 Content 속성에 직접 설정되므로 Label에 이름이 필요하지 않으므로 다음과 같이 나타낼 수도 있습니다.
namespace Greetings
{
    public class GreetingsPage : ContentPage
    {
        public GreetingsPage()
        {
            Content = new Label {
                Text = "Greetings Xamarin.Forms!"
            };                        
        }
    }
}
cs

보다 복잡한 페이지 레이아웃의 경우 이런 스타일은 페이지의 레이아웃 및 뷰 구성을 시각적으로 판단하기에 용이합니다. 그러나 메소드를 호출하거나 이벤트 핸들러를 설정해야 하는 경우 언제나 이 예제처럼 간단하지는 않습니다.

어떤 방법 으로든 에뮬레이터 나 장치의 iOS, Android 및 Windows 10 Mobile 플랫폼에서 프로그램을 성공적으로 컴파일하고 실행할 수있는 경우 다음과 같이 표시됩니다

이 Greetings 솔루션 중에 가장 실망스러운 버전은 단연 iPhone입니다. iOS7 싱글페이지 앱에서 상태바와 글씨가 상단에서 겹쳐서 나타납니다. 앱에서 뭔가 보상해주지 않는다면 언제나 저렇게 겹쳐서 나타날 것입니다.

이 현상은 후에 multipage-navigation 앱을 만들게 되면 사라지는데, 그때까지 이 현상을 해결할 방법은 네가지(SAP를 이용한다면 다섯 가지)가 있습니다.

해결법 1. 페이지에 여백 포함

Page 클래스는 내용이 침입 할 수없는 페이지의 내부 둘레를 표시하는 Padding이라는 속성을 정의합니다. Padding 속성은 Left, Top, Right, Bottom이라는 네 개의 속성을 정의하는 구조체인 Thickness 타입입니다.(Thickness 생성자 뿐만 아니라 XAML에서도 속성을 정의하는 순서이기 때문에 해당 값 순서를 기억해 두는 것이 좋을 수 있습니다.) Thickness 구조체는 네면 모두에서 동일한 양의 패딩을 설정하거나 왼쪽-오른쪽, 위쪽-아래쪽에 같은 양을 설정하는 생성자를 정의합니다. 

검색을 해보면 iOS 상태 표시 줄의 높이가 20임을 알 수 있습니다(20? 20 픽셀? 지금은 그냥 20unit이라 생각하십시오. -이것은 서로 다른 기기의 크기에 대응하는 애플 고유의 크기 정의에 따른 것입니다. 자마린 프로그래밍에 있어서 대부분의 경우엔 숫자 크기로 신경쓸 이유는 없습니다만, Chapter5. Dealing with sizes에서는 pixel레벨의 안내가 있을 것입니다.)

다음과 같이 하여 상태 표시줄을 조정할 수 있습니다.
namespace Greetings
{
    public class GreetingsPage : ContentPage
    {
        public GreetingsPage()
        {
            Content = new Label {
                Text = "Greetings Xamarin.Forms!"
            };
            Padding = new Thickness(02000);               
        }
    }
}
cs

그러면 상단으로부터 20unit 떨어진 곳에 인사말이 나타납니다.


Padding을 조절하여 iOS의 겹침 현상을 해결했지만, 다른 OS에서도 20unit 아래로 글씨가 옮겨집니다. 그렇게 하는 것이 적절치 않다고 생각될 경우엔 어떻게 할까요?

해결법 2. iOS만의 Padding 추가(SAP에서만 가능)

SAP (Shared Asset Project) 방식의 장점 중 하나는 프로젝트의 클래스가 응용 프로그램 프로젝트의 확장이므로 조건부 컴파일 지시문을 사용할 수 있다는 것입니다. 

이렇게 해 봅시다. SAP 템플릿을 기반으로 한 GreetingsSap이라는 새로운 솔루션과 GreetingsSapPage라는 GreetingsSap 프로젝트의 새로운 페이지 클래스가 필요합니다.
프로젝트를 생성했으면 iOS만의 Padding을 위해 다음과 같은 지시문을 추가합니다.
namespace GreetingsSap
{
    public class GreetingsSapPage : ContentPage
    {
        public GreetingsSapPage ()
        {
            Content = new Xamarin.Forms.Label
            {
                Text = "Greetings, Xamarin.Forms!"
            };
#if __IOS__
            Padding = new Thickness(0,20,0,0);
#endif
        }
    }
}
cs

#if 지시문은 조건부 컴파일 기호 __IOS__를 참조하므로 Padding 속성은 iOS 프로젝트에만 설정됩니다. 결과는 다음과 같습니다.

그러나 이러한 조건부 컴파일 기호는 프로그램 컴파일에만 영향을 미치므로 PCL에는 영향을 미치지 못합니다. PCL 프로젝트가 서로 다른 플랫폼에 각각 Padding을 가질 수는 없을까요?

해결법 3. iOS만의 Padding포함(PCL, SAP 공통)

물론 방법이 있습니다. 정적 Device 클래스는 런타임시 기기 차이를 매우 간단하고 직관적인 방법으로 처리 할 수있는 몇 가지 속성과 메서드를 가지고 있습니다.
  • Device.OS 프로퍼티는 TargetPlatform 열거형의 멤버(iOS, Android, WinPhone, Other)를 반환합니다. WinPhone은 모든 윈도우즈와 윈도우즈폰 멤버를 말합니다.
  • Device.Idiom 프로퍼티는 TargetIdiom 열거형의 멤버(Phone, Tablet, Desktop, Unsupported)를 반환합니다.
이 두가지 속성과 if~else문, switch~case문을 가지고서 플랫폼 특화된 코드를 실행할 수 있습니다.

OnPlatform이라는 이름의 두 메소드는 좀 더 우아한 방법을 제공합니다.

  • static generic 메소드인 OnPlatform<T>는 세 개의 type T 인자를 받습니다. 순서대로 iOS, Android, Windows이며, 실행 중인 플랫폼에 대한 인자를 반환합니다.
  • static 메소드인 OnPlatform은 네 개의 Action(인자를 받지 않고 void를 반환하는 .NET function delegate를 말합니다)  타입 인자를 가지고, 그 순서는 iOS, Android, Windows Phone, default입니다.
세 가지 플랫폼 모두에서 동일한 Padding 속성을 설정하는 대신 Device.OnPlatform 일반 메서드를 사용하여 Padding을 iPhone으로만 제한 할 수 있습니다.

Padding = Device.OnPlatform<Thickness>(new Thickness(02000),
                new Thickness(0),
                new Thickness(0));
cs

명시적으로 타입을 넣지 않아도 컴파일러가 유추할 수 있으므로 다음과 같이 해도 무방합니다.
Padding = Device.OnPlatform(new Thickness(02000),
                new Thickness(0),
                new Thickness(0));
cs

또는 하나의 Thickness 생성자를 가지고서 다음처럼 할 수도 있습니다.
Padding = new Thickness(0, Device.OnPlatform(2000), 00);
cs

이상이 Padding 속성을 사용하는 방법입니다. 물론 숫자는 임의로 바꾸실 수 있습니다.

그러나 단순히 iOS의 Padding 값만을 바꾸고자 한다면 Action인자를 가지는 Device.OnPlatform 버전을 사용하는게 좋습니다. Action 인자의 default값은 null이며, 여러분은 그냥 iOS에서 실행될 첫번째 action을 설정하기만 하면 됩니다.
public class GreetingsPage : ContentPage
{
    public GreetingsPage()
    {
        Content = new Label {
            Text = "Greetings Xamarin.Forms!"
        };
        Device.OnPlatform(() =>
        {
            Padding = new Thickness(02000);
        });               
    }
}
cs

이렇게 해서 프로그램이 iOS에서 구동될 때에만 실행되는 Padding을 설정할 수 있습니다. 그러나 가독성을 위해 명시적으로 iOS: 라는 parameter 이름을 설정할 수 있습니다.
Device.OnPlatform(iOS:() =>
{
    Padding = new Thickness(02000);
});      
cs
인자 이름을 붙이는 것은 C#4.0에서 도입된 방법입니다.

Device.OnPlatform 메소드는 매우 편리하며 PCL과 SAP 프로젝트 모두에서 작업할 수있다는 이점이 있습니다. 그러나 개별 플랫폼 내에서 API에 액세스 할 수는 없습니다. 이를 위해서는 9 장에서 논의되는 DependencyService가 필요합니다.

해결법 4. 라벨을 페이지 중앙에.

상태바와 라벨이 겹친 것은 문구가 좌상단에 있었기 때문입니다. 그럼 라벨을 가운데에 위치시키려면 어떻게 하면 될까요?

Xamarin.Forms는 프로그램이 크기 및 좌표와 관련된 계산을 일일이 수행하지 않고도, 레이아웃을 쉽게 할 수 있는 많은 기능을 지원합니다. View 클래스는 HorizontalOptionsVerticalOptions라는 두 개의 속성을 정의합니다. 이 속성은 뷰가 부모 (이 경우 ContentPage)를 기준으로 배치되도록 해줍니다. 이 두 속성은 Xamarin.Forms의 매우 중요한 구조 인 LayoutOptions 타입입니다. 

일반적으로 LayoutOptions 구조체를 반환하는 8 개의 public static 읽기 전용 필드 중 하나를 지정하여 구조체를 사용합니다.
  • Start
  • Center
  • End
  • Fill
  • StartAndExpand
  • CenterAndExpand
  • EndAndExpand
  • FillAndExpand
그러나 LayoutOptions 값을 직접 만들 수도 있습니다. LayoutOptions은 조합하여 값을 만들 수있는 두 개의 인스턴스 속성을 정의합니다.

  • Alignment 프로퍼티: 4 개의 멤버(Start, Center, End, Fill)를 가지는 열거형 LayoutAlignment
  • Expand 프로퍼티: bool값을 가짐

이 모든 옵션에 대한 자세한 설명은 4 장 "Scrolling the stack"에서 설명하므로, 지금은 LabelHorizontalOptionsVerticalOptions 속성을 설정하도록 합니다. HorizontalOptions의 경우 Start는 왼쪽을 의미하고 End는 오른쪽을 의미합니다. VerticalOptions의 경우 Start는 위쪽을 의미하고 End는 아래쪽을 의미합니다.

HorizontalOptions와 VerticalOptions 두 속성을 마스터 하는 것은 자마린 레이아웃 시스템에서 중요한 것이지만 여기서는 Label을 센터에 두는 간단한 예제만을 소개합니다.
public class GreetingsPage : ContentPage
{
    public GreetingsPage()
    {
        Content = new Label
        {
            Text = "Greetings Xamarin.Forms!",
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.Center
        };               
    }
}
cs

옵션을 조합하면 페이지 상의 아홉 군데에 라벨이 위치하도록 할 수 있습니다.

해결법 5. 라벨 내에서 text가 중앙에 위치하도록

레이블은 최대 단락(paragraph)까지 텍스트를 표시하기 위한 용도이며, 텍스트 줄이 가로로 정렬되는 방식 (왼쪽 맞춤, 오른쪽 맞춤 또는 가운데 맞춤)을 제어하는 ​​것이 바람직 합니다. 그런 용도로 쓰기 위해 HorizontalTextAlignment 속성과 VerticalTextAlignment 속성이 Label 뷰 내에 정의되어 있습니다. 두 속성은 TextAlignment 열거형(Start, CenterEnd라는 멤버가있는)의 멤버로 설정되어 있습니다. 여기서 Start는 왼쪽 또는 위를 의미하고, End는 오른쪽 또는 아래를 의미합니다.


iOS 상태 표시줄 문제를 풀기 위한 마지막 해법으로서, HorizontalTextAlignmentVerticalTextAlignmentTextAlignment.Center로 설정하십시오.

public class GreetingsPage : ContentPage
{
    public GreetingsPage()
    {
        Content = new Label
        {
            Text = "Greetings Xamarin.Forms!",
            HorizontalTextAlignment = TextAlignment.Center,
            VerticalTextAlignment = TextAlignment.Center
        };        
    }
}
cs

시각적으로 이 결과는 앞에서 HorizontalOptions와 VerticalOptions를 가지고서 한 것과 같으며, 역시 조합에 따라 9군데의 디스플레이 위치를 지정할 수 있습니다.

그러나 위의 두가지 방법은 실상 매우 다른 것으로서 다음 장에서 설명 드릴 것입니다.

댓글 없음:

댓글 쓰기