사진 라이브러리에서 사진 선택

이 문서에서는 사용자가 휴대폰의 사진 라이브러리에서 사진을 선택할 수 있는 애플리케이션 만들기를 설명합니다. Xamarin.Forms에는 이 기능이 포함되지 않으므로 DependencyService를 사용하여 각 플랫폼에서 기본 API에 액세스해야 합니다.

인터페이스 만들기

먼저, 원하는 기능이 표시되는 공유 코드에서 인터페이스를 만듭니다. 사진 선택 애플리케이션의 경우 하나의 메서드만 필요합니다. 이는 샘플 코드의 IPhotoPickerService .NET Standard 라이브러리에 있는 인터페이스에서 정의됩니다.

namespace DependencyServiceDemos
{
    public interface IPhotoPickerService
    {
        Task<Stream> GetImageStreamAsync();
    }
}

GetImageStreamAsync 메서드는 해당 메서드가 신속하게 반환해야 하므로 비동기로 정의되지만 사용자가 사진 라이브러리를 검색하여 사진 하나를 선택하기까지 선택된 사진에 대한 Stream 개체를 반환할 수 없습니다.

이 인터페이스는 플랫폼별 코드를 사용하여 모든 플랫폼에서 구현됩니다.

iOS 구현

IPhotoPickerService 인터페이스의 iOS 구현은 갤러리에서 사진 선택 레시피 및 샘플 코드에 설명된 대로 UIImagePickerController를 사용합니다.

iOS 구현은 샘플 코드의 iOS 프로젝트에서 PhotoPickerService 클래스에 포함됩니다. 이 클래스를 DependencyService 관리자에 표시하려면 해당 클래스를 Dependency 유형의 [assembly] 특성을 사용하여 식별해야 합니다. 해당 클래스는 공개되어야 하며 명시적으로 IPhotoPickerService 인터페이스를 구현해야 합니다.

[assembly: Dependency (typeof (PhotoPickerService))]
namespace DependencyServiceDemos.iOS
{
    public class PhotoPickerService : IPhotoPickerService
    {
        TaskCompletionSource<Stream> taskCompletionSource;
        UIImagePickerController imagePicker;

        public Task<Stream> GetImageStreamAsync()
        {
            // Create and define UIImagePickerController
            imagePicker = new UIImagePickerController
            {
                SourceType = UIImagePickerControllerSourceType.PhotoLibrary,
                MediaTypes = UIImagePickerController.AvailableMediaTypes(UIImagePickerControllerSourceType.PhotoLibrary)
            };

            // Set event handlers
            imagePicker.FinishedPickingMedia += OnImagePickerFinishedPickingMedia;
            imagePicker.Canceled += OnImagePickerCancelled;

            // Present UIImagePickerController;
            UIWindow window = UIApplication.SharedApplication.KeyWindow;
            var viewController = window.RootViewController;
            viewController.PresentViewController(imagePicker, true, null);

            // Return Task object
            taskCompletionSource = new TaskCompletionSource<Stream>();
            return taskCompletionSource.Task;
        }
        ...
    }
}

GetImageStreamAsync 메서드는 사진 라이브러리에서 이미지를 선택하려면 UIImagePickerController를 만들고 초기화합니다. 두 개의 이벤트 처리기가 필요합니다. 하나는 사용자가 사진을 선택할 경우 필요하며 다른 하나는 사용자가 사진 라이브러리의 표시를 취소하는 경우 필요합니다. PresentViewController 메서드는 사용자에게 사진 라이브러리를 표시합니다.

이 시점에서 GetImageStreamAsync 메서드는 Task<Stream> 개체를 호출하는 코드에 해당 개체를 반환해야 합니다. 이 태스크는 사용자가 사진 라이브러리와 상호 작용을 완료하고 이벤트 처리기 중 하나를 호출하는 경우에만 완료됩니다. 이와 같은 경우에 TaskCompletionSource 클래스는 필수입니다. 클래스는 GetImageStreamAsync 메서드에서 반환할 적절한 제네릭 형식의 Task 개체를 제공하며, 해당 클래스는 태스크를 완료한 경우 나중에 신호를 받을 수 있습니다.

FinishedPickingMedia 이벤트 처리기는 사용자가 사진을 선택한 경우 호출됩니다. 하지만 해당 처리기는 UIImage 개체를 제공하고 Task는 .NET Stream 개체를 반환해야 합니다. 이 작업은 두 단계로 수행됩니다. 개체는 UIImage 먼저 개체에 저장된 메모리 내 PNG 또는 JPEG 파일로 변환된 NSData 다음 NSData 개체가 .NET Stream 개체로 변환됩니다. TaskCompletionSource 개체의 SetResult 메서드에 대한 호출은 Stream 개체를 제공하여 태스크를 완료합니다.

namespace DependencyServiceDemos.iOS
{
    public class PhotoPickerService : IPhotoPickerService
    {
        TaskCompletionSource<Stream> taskCompletionSource;
        UIImagePickerController imagePicker;
        ...
        void OnImagePickerFinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs args)
        {
            UIImage image = args.EditedImage ?? args.OriginalImage;

            if (image != null)
            {
                // Convert UIImage to .NET Stream object
                NSData data;
                if (args.ReferenceUrl.PathExtension.Equals("PNG") || args.ReferenceUrl.PathExtension.Equals("png"))
                {
                    data = image.AsPNG();
                }
                else
                {
                    data = image.AsJPEG(1);
                }
                Stream stream = data.AsStream();

                UnregisterEventHandlers();

                // Set the Stream as the completion of the Task
                taskCompletionSource.SetResult(stream);
            }
            else
            {
                UnregisterEventHandlers();
                taskCompletionSource.SetResult(null);
            }
            imagePicker.DismissModalViewController(true);
        }

        void OnImagePickerCancelled(object sender, EventArgs args)
        {
            UnregisterEventHandlers();
            taskCompletionSource.SetResult(null);
            imagePicker.DismissModalViewController(true);
        }

        void UnregisterEventHandlers()
        {
            imagePicker.FinishedPickingMedia -= OnImagePickerFinishedPickingMedia;
            imagePicker.Canceled -= OnImagePickerCancelled;
        }
    }
}

iOS 애플리케이션에는 휴대폰의 사진 라이브러리에 액세스하려면 사용자의 권한이 필요합니다. 다음을 Info.plist 파일의 dict 섹션에 추가합니다.

<key>NSPhotoLibraryUsageDescription</key>
<string>Picture Picker uses photo library</string>

Android 구현

Android 구현은 이미지 선택 레시피 및 샘플 코드에 설명된 기술을 사용합니다. 그러나 사용자가 사진 라이브러리에서 이미지를 선택한 경우 호출되는 메서드는 Activity에서 파생된 클래스의 OnActivityResult 재정의입니다. 따라서 Android 프로젝트의 기본 MainActivity 클래스는 OnActivityResult 메서드의 필드, 속성 및 재정의를 통해 보완되었습니다.

public class MainActivity : FormsAppCompatActivity
{
    internal static MainActivity Instance { get; private set; }  

    protected override void OnCreate(Bundle savedInstanceState)
    {
        // ...
        Instance = this;
    }
    // ...
    // Field, property, and method for Picture Picker
    public static readonly int PickImageId = 1000;

    public TaskCompletionSource<Stream> PickImageTaskCompletionSource { set; get; }

    protected override void OnActivityResult(int requestCode, Result resultCode, Intent intent)
    {
        base.OnActivityResult(requestCode, resultCode, intent);

        if (requestCode == PickImageId)
        {
            if ((resultCode == Result.Ok) && (intent != null))
            {
                Android.Net.Uri uri = intent.Data;
                Stream stream = ContentResolver.OpenInputStream(uri);

                // Set the Stream as the completion of the Task
                PickImageTaskCompletionSource.SetResult(stream);
            }
            else
            {
                PickImageTaskCompletionSource.SetResult(null);
            }
        }
    }
}

OnActivityResult 재정의는 Android Uri 개체를 사용하여 선택한 사진 파일을 나타내지만 활동의 ContentResolver 속성에서 가져온 ContentResolver 개체의 OpenInputStream 메서드를 호출하여 이 파일을 .NET Stream 개체로 변환할 수 있습니다.

iOS 구현과 같이 Android 구현은 태스크가 완료된 경우 TaskCompletionSource를 사용하여 신호를 보냅니다. 이 TaskCompletionSource 개체는 MainActivity 클래스의 공용 속성으로 정의됩니다. 이렇게 하면 Android 프로젝트의 PhotoPickerService 클래스에서 속성을 참조할 수 있습니다. 이 클래스는 GetImageStreamAsync 메서드를 사용합니다.

[assembly: Dependency(typeof(PhotoPickerService))]
namespace DependencyServiceDemos.Droid
{
    public class PhotoPickerService : IPhotoPickerService
    {
        public Task<Stream> GetImageStreamAsync()
        {
            // Define the Intent for getting images
            Intent intent = new Intent();
            intent.SetType("image/*");
            intent.SetAction(Intent.ActionGetContent);

            // Start the picture-picker activity (resumes in MainActivity.cs)
            MainActivity.Instance.StartActivityForResult(
                Intent.CreateChooser(intent, "Select Picture"),
                MainActivity.PickImageId);

            // Save the TaskCompletionSource object as a MainActivity property
            MainActivity.Instance.PickImageTaskCompletionSource = new TaskCompletionSource<Stream>();

            // Return Task object
            return MainActivity.Instance.PickImageTaskCompletionSource.Task;
        }
    }
}

이 메서드는 Instance 속성, PickImageId 필드, TaskCompletionSource 속성을 위해, StartActivityForResult를 호출하기 위해 등 여러 목적으로 MainActivity 클래스에 액세스합니다. 이 메서드는 MainActivity의 기본 클래스인 FormsAppCompatActivity 클래스에 따라 정의됩니다.

UWP 구현

iOS 및 Android 구현과 달리 유니버설 Windows 플랫폼에 대한 사진 선택기 구현에는 TaskCompletionSource 클래스가 필요하지 않습니다. PhotoPickerService 클래스는 FileOpenPicker 클래스를 사용하여 사진 라이브러리에 액세스합니다. FileOpenPickerPickSingleFileAsync 메서드 자체는 비동기이므로 GetImageStreamAsync 메서드는 해당 메서드(및 다른 비동기 메서드)를 통해 await를 사용하고 Stream 개체를 반환할 수 있습니다.

[assembly: Dependency(typeof(PhotoPickerService))]
namespace DependencyServiceDemos.UWP
{
    public class PhotoPickerService : IPhotoPickerService
    {
        public async Task<Stream> GetImageStreamAsync()
        {
            // Create and initialize the FileOpenPicker
            FileOpenPicker openPicker = new FileOpenPicker
            {
                ViewMode = PickerViewMode.Thumbnail,
                SuggestedStartLocation = PickerLocationId.PicturesLibrary,
            };

            openPicker.FileTypeFilter.Add(".jpg");
            openPicker.FileTypeFilter.Add(".jpeg");
            openPicker.FileTypeFilter.Add(".png");

            // Get a file and return a Stream
            StorageFile storageFile = await openPicker.PickSingleFileAsync();

            if (storageFile == null)
            {
                return null;
            }

            IRandomAccessStreamWithContentType raStream = await storageFile.OpenReadAsync();
            return raStream.AsStreamForRead();
        }
    }
}

공유 코드에서 구현

각 플랫폼에 대해 인터페이스를 구현했으므로 이제 .NET Standard 라이브러리의 공유 코드는 해당 인터페이스를 활용할 수 있습니다.

UI에는 클릭하여 사진을 선택할 수 있는 Button이 포함됩니다.

<Button Text="Pick Photo"
        Clicked="OnPickPhotoButtonClicked" />

Clicked 이벤트 처리기는 DependencyService 클래스를 사용하여 GetImageStreamAsync를 호출합니다. 이렇게 하면 플랫폼 프로젝트에서 호출이 발생합니다. 메서드가 Stream 개체를 반환하는 경우 처리기는 image 개체의 Source 속성을 Stream 데이터로 설정합니다.

async void OnPickPhotoButtonClicked(object sender, EventArgs e)
{
    (sender as Button).IsEnabled = false;

    Stream stream = await DependencyService.Get<IPhotoPickerService>().GetImageStreamAsync();
    if (stream != null)
    {
        image.Source = ImageSource.FromStream(() => stream);
    }

    (sender as Button).IsEnabled = true;
}