スタンプカメラアプリ

講義の目標

  • 基本的なカメラ機能を実装できるようになること
  • アクションシートを実装できるようになること

スタンプカメラアプリの概要

iPhoneアプリのなかでも特に人気が高いのはカメラ機能を使ったアプリです。FacebookやLINEなどのSNSでは、写真を使ったコミュニケーションが一般化しています。また多くの写真フィルタを搭載した画像共有SNS・インスタグラムはApp Storeに登場してから、瞬く間に世界を席巻しました。今後も様々な写真を使ったコミュニケーションが、iPhoneを使って行われていくでしょう。

この章ではカメラアプリのなかでも人気の高いスタンプカメラを制作します。スタンプカメラは、アプリ内で保有しているスタンプ画像を写真の上で好きなところに配置し、合成画像が作れるアプリです。

このアプリでは以下の機能を実装します。

  • iPhoneやiPadのカメラで撮影した写真、もしくは端末内の写真をアプリ内に取り込む機能
  • コレクションビューを使った、スタンプ一覧の表示。
  • 選択したスタンプ画像を写真上の好きな位置に、一本指でドラッグアンドドロップできる機能

尚、写真を撮影する機能は実機のiPhoneがないとシミュレーションすることは出来ませんが、スタンプカメラならカメラロールに保存されている写真でシミュレートをすることが出来ます。

このスタンプカメラの開発をマスターすれば、スタンプ画像を好みのスタンプに入れ替えることで、オリジナルのカメラアプリを作ることができます。ぜひ、トライしてみてくださいね。

完成イメージ

完成品のサンプルは以下よりダウンロードできます。イメージが湧かない方は是非ご確認ください。

iOS完成品サンプル(スタンプカメラ)

それでは、早速開発を始めて行きましょう!

プロジェクトの作成

Xcodeを起動し、新規プロジェクトの作成を行います。テンプレートは「Single View Application」を選択します。

設定画面で下記の設定を行い、任意の場所にプロジェクトを作成します。

入力項目 入力値
Product Name StampCamera
Team None
Oganization Name 任意(”ALJ”など自分が所属している組織の名前を入れる)
Oganization Identifier jp.co.al-j
Language Swift
Devices iPhone
Use Core Data チェックを外す
include Unit Tests チェックを外す
include UI Tests チェックを外す

アプリの設定

新規プロジェクトを立ち上げたら、アプリの設定を行います。ここで、アプリのアイコンやアプリの表示タイトル(Bundle Name)、サポートするデバイスの向き(Device Orientation)を以下のように、設定します。

まずは、下記リンクから素材をダウンロードしてください。

素材ダウンロード

アイコン設定

アプリの設定画面のDeployment InfoでDevice OrientationのPortraitのみチェックを入れます。

スプラッシュ画像設定

画像素材のsplashフォルダの中のスプラッシュ画像をプロジェクトに取り込んでください。

画像設定

アプリ内で使用する素材をプロジェクトに取り込みます。

「image」フォルダ内のファイルをAssets.xcassetsフォルダにドラッグしてください。

ホーム画面のアプリタイトル

Project Editorの「info」タブをクリックして「Bundle Name」の項目を次のように変更していきます。

設定項目 設定値
Bundle Name スタンプカメラ

インターフェイス画面の作成

今回のアプリの開発では、インターフェイス画面を作るためにストーリーボードを使いますが、オートレイアウトは使いません。スタンプカメラは配置したスタンプ画像を画面上で自由に動かします。このように動的に画面の構成要素を動かす必要があるアプリの場合は、オートレイアウトを使わない方が効率的に開発ができるからです。

オートレイアウトを使わない場合は、ストーリーボードで設定をする必要があります。

Main.storyboardで、View Controllerを選択してFile Inspectorで、「Use Auto Layout」のチェックを外します。

File Inspectorでは選択しているファイルの情報や設定を行うことができます。

アラートが表示されるので「Disable Size Classes」ボタンをクリックします。

Object LibraryからViewを選択し、View Controllerにドラッグ&ドロップします。

このViewは画像やスタンプを表示するための下地になります。

ViewのサイズをSize InspectorでShowを「Frame Rectangle」に設定してフレームを下記のように設定します。またAutoresizingを右の画像のように設定します。

Viewのフレーム設定

X:0 Y:0 Width:375 Height:623

Image Viewを配置したViewの上に配置します。X、Y、Width、Heightは下地のViewと同じにします。

またAttributes InspectorでViewのModeを「Aspect Fill」にします。

DocumentOutlineの階層を見ると配置したImage ViewがViewのサブビューになっているのがわかります。

ツールバーを画面下に配置します。

このツールバーは下地のViewのサブビューにしないようにしましょう。

ツールバーのサイズはSize InspectorのShowを「Frame Rectangle」に設定してフレームを下記のように設定します。またAutoresizingを右の画像のように設定します。

ツールバーのフレーム設定

X:0 Y:524 Width:320 Height:44

ツールバー上にBar Button Itemを4つ配置します。

最初からひとつ配置されていますので、新たに3つ追加することになります。

Bar Button Itemの間にFlexible Space Bar Button Itemを入れます。

Flexible Space Bar Button Itemを入れることによりBar Button Itemが間隔が均等になります。

各Bar Button ItemのアイコンをAttributes InspectorのIdentifierで設定します。

Bar Button Itemの左からCamera(カメラ機能)、Action(スタンプ選択)、Trash(スタンプの削除)、Organize(画像の保存)のIdentifierを設定しています。

ViewのAutoresizingを上のように設定しましたが、これにより、画面サイズが異なる端末でも、画面下部を除いて常に画面はViewに覆われることになります。
このViewの上に、Image Viewを下地のViewのサブビューになるように配置しましたが、これはカメラで撮影した写真や、デバイスに保存されている写真の表示を行うオブジェクトとなります。

設定をAspect Fillにすることによって、端末サイズが異なっても写真の縦横比が正しいままで画面上に表示されます。

ツールバーのAutoresizingは上のように設定しました。これにより画面サイズが変化してもツールバーの幅が画面の幅にフィットするようになります。しかし高さは、画面サイズが変化しても変わりません。

カメラ機能の実装

このアプリではカメラボタンをタップすると、カメラの起動か、フォトライブラリへのアクセスかをユーザーに選択してもらいます。この選択肢の表示はアクションシート(UIActionSheet)を使用します。

選択肢を表示するためのアクションシート

アクションシートはユーザーに選択肢を表示させたい場合や、どのような処理を実行するかを確認する場合に使用します。アクションシートを利用するにはUIAlertControllerを利用します。それではツールバーに配置したカメラボタンをタップしたら、アクションシートを表示するcameraTappedメソッドを記述しましょう。ストーリーボードで接続するので@IBActionを付けるのを忘れないようにしてください。

ViewController.swift

//アクションシート表示メソッド
@IBAction func cameraTapped(){
// UIActionSheet生成
let actionSheet:UIAlertController = UIAlertController(title:"写真を取得",
message: "写真の取得先を選択してください",
preferredStyle: UIAlertControllerStyle.actionSheet)
// Cancelボタン
let cancelAction:UIAlertAction = UIAlertAction(title: "Cancel",
style: UIAlertActionStyle.cancel,
handler:{
(action:UIAlertAction!) -> Void in
print("Cancel")
})
// Cameraボタン
let cameraAction:UIAlertAction = UIAlertAction(title: "Camera",
style: UIAlertActionStyle.default,
handler:{
(action:UIAlertAction!) -> Void in
print("Camera")
})
// Libraryボタン
let libraryAction:UIAlertAction = UIAlertAction(title: "Library",
style: UIAlertActionStyle.default,
handler:{
(action:UIAlertAction!) -> Void in
print("Library")
})
// AlertViewControllerにボタンを追加
actionSheet.addAction(cancelAction)
actionSheet.addAction(cameraAction)
actionSheet.addAction(libraryAction)
// 画面表示
present(actionSheet, animated: true, completion: nil)
}

UIActionSheetではaddButtonWithTitleでボタンを追加することができます。またcancel-ButtonIndexで追加したボタンのインデックス番号を指定すれば、アクションシートを閉じるためのキャンセルボタンが作成されます。

cameraTappedメソッドで注意してもらいたいのはsheet.delegate = selfです。プロトコルの指定だけでは、デリゲートメソッドを使用することはできません。UIActionSheetのデリゲートを自クラスに設定することでデリゲートメソッドが使えます。これによりこの後解説するactionSheetメソッドが、アクションシートのボタンをタップしたタイミングで呼び出されるようになります。

それでは、このメソッドをストーリーボード上でカメラのBar Button Itemと接続してください。

Received ActionsにViewController.Swiftで宣言したcameraTappedメソッドが表示されているので、右の(◯)をドラッグして接続します。

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

iOSシミュレータを起動して確認しましょう。カメラボタンをクリックするとアクションシートが表示されます。またキャンセルボタンをクリックすると、アクションシートが閉じます。

画像を取得するためのUIImagePickerController

カメラやフォトライブラリから画像を取得するためにはUIImagePickerControllerクラスを使用します。UIImagePickerControllerもデリゲートメソッドを使用して、カメラ画面、フォトライブラリ画面を表示させます。この画面遷移にUINavigationControllerを使用しており、UIImagePickerControllerのデリゲートメソッドを使うためには、UIImagePickerControllerDelegateだけでなく、UINavigatio-nControllerDelegateのプロトコルも追加指定しなければいけません。

ViewController.swift

//////////////// ▼▼ プロトコル追加 ▼▼ ////////////////
class ViewController: UIViewController,UIActionSheetDelegate,UIImagePickerControllerDelegate, UINavigationControllerDelegate{
//////////////// ▲▲ プロトコル追加 ▲▲ ////////////////

//////////////// ▼▼ 追加 ▼▼ ////////////////
var pickerController = UIImagePickerController()
//////////////// ▲▲ 追加 ▲▲ ////////////////

override func viewDidLoad() {
super.viewDidLoad()
//////////////// ▼▼ 追加 ▼▼ ////////////////
//UIImagePickerControllerのデリゲートメソッドを使用する設定
pickerController.delegate = self
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
:
:
}

それではUIActionSheetのデリゲートメソッドactionSheetでUIImagePickerControlerのインスタンスを作成し、アクションシートの1番目のボタンをタップしたらカメラ画面に、2番目のボタンをタップしたらフォトライブラリ画面に遷移させましょう。

ViewController.swift

//アクションシート表示メソッド
@IBAction func cameraTapped(){
// UIActionSheet生成
let actionSheet:UIAlertController = UIAlertController(title:"写真を取得",
message: "写真の取得先を選択してください",
preferredStyle: UIAlertControllerStyle.ActionSheet)
// Cancelボタン
let cancelAction:UIAlertAction = UIAlertAction(title: "Cancel",
style: UIAlertActionStyle.Cancel,
handler:{
(action:UIAlertAction!) -> Void in
print("Cancel")
})
// Cameraボタン
let cameraAction:UIAlertAction = UIAlertAction(title: "Camera",
style: UIAlertActionStyle.Default,
handler:{
(action:UIAlertAction!) -> Void in
print("Camera")
//////////////// ▼▼ 追加 ▼▼ ////////////////
//1番目のボタンを押したらソースタイプをカメラに設定
self.pickerController.sourceType = UIImagePickerControllerSourceType.camera
//UIImagePickerControllerを表示
self.present(self.pickerController, animated: true, completion: nil)
//////////////// ▲▲ 追加 ▲▲ ////////////////
})
// Libraryボタン
let libraryAction:UIAlertAction = UIAlertAction(title: "Library",
style: UIAlertActionStyle.Default,
handler:{
(action:UIAlertAction!) -> Void in
print("Library")
//////////////// ▼▼ 追加 ▼▼ ////////////////
//2番目のボタンを押したらソースタイプをフォトライブラリに設定
self.pickerController.sourceType = UIImagePickerControllerSourceType.photoLibrary
//UIImagePickerControllerを表示
self.present(self.pickerController, animated: true, completion: nil)
//////////////// ▲▲ 追加 ▲▲ ////////////////
})
// AlertViewControllerにボタンを追加
actionSheet.addAction(cancelAction)
actionSheet.addAction(cameraAction)
actionSheet.addAction(libraryAction)
// 画面表示
presentViewController(actionSheet, animated: true, completion: nil)
}

アクションシートのボタンをタップすると、そのインデックス番号がactionSheetメソッドの引数buttonIndexに格納されます。これにより、カメラ機能を用いるのか、画像選択機能を用いるのかをif-elseによる分岐を用いて選択しています。また、ここでもpickerController.delegate = selfでデリゲートの設定をしています。これにより後述するimagePickerControllerメソッドが、カメラ画面が閉じるタイミングで呼ばれることになります。そして、presentViewControllerメソッドにより、カメラ画面、もしくは画像選択画面が表示されることになります。

iOS10から「Info.plist」にフォトライブラリへアクセスしていいのか許可を取るための設定を行わないといけなくなりました。
なので「Supporting Files」の中にある「Info.plist」を右クリックして開いてください。

次に以下の要素を追加します。

  • NSPhotoLibraryUsageDescription
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ▼▼ 追加 ▼▼-->
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo Library</string>
<key>NSCameraUsageDescription</key>
<string>Camera</string>
<!-- ▲▲ 追加 ▲▲-->
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

設定はこれで完了です。

iOSシミュレータを起動してみましょう。カメラを使うことは出来ませんが、フォトライブラリにはアクセスすることができます。

UIImagePickerControllerで取得した画像を表示する

カメラ、もしくはフォトライブラリにアクセスすることは出来ました。次は画像を取得し、UIImage-Viewを使って表示しましょう。まず、メンバ変数mainImageViewを宣言しましょう。

ViewController.swift

//UIImagePickerControllerで取得した画像を表示
@IBOutlet var mainImageView:UIImageView!

画像を取得するにはUIImagePickerControlerのデリゲートメソッドimagePickerControllerを使用します。このメソッドのなかでmainImageViewに取得した画像を設定し、カメラ画面もしくはフォトライブラリ画面を閉じます。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//UIImagePickerController画像取得メソッド
func imagePickerController(_ imagePicker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {

if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
mainImageView.image = pickedImage

}
//閉じる処理
imagePicker.dismiss(animated: true, completion: nil)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

OutletsにViewController.Swiftで宣言したmainImageViewが表示されているので、右の(◯)をドラッグして接続します。

それではシミュレータもしくは実機で確認してみてください。取得した画像が画面に表示されます。

スタンプ画像を配置して合成画像を作る

スタンプ選択画面の作成

カメラを使った画像の取得、そしてフォトライブラリからの画像の取得を行うことができました。次はスタンプ画像を選択し、その画像を画面内に自由にドラッグして配置するという機能を実装します。

スタンプ一覧画面はコレクションビュー(Collection View)を使って作成します。まずは、Main.storyboardで新しくView Controllerを配置しましょう。

Main.storyboardで、Object LibraryからUIViewControllerを配置します。

最初にあったView Controllerのアイコンを選択し、[control]キーを押しながら新規に配置したView Controllerにドラッグします。ドロップすると、選択肢が表示されますので、「Present Modally」を選択します。

Segueアイコン()を選択し、Attributes Inspector()で、Identifierに「ToStampList」と名前を付けます。

新規に追加したView ControllerにObject LibraryからCollectionViewをドラッグします。下にボタンを配置するので、画面全体を覆わずに少しスペースを空けておきます。

CollectionViewには最初からCollectionViewCellがひとつ配置されています。Document Outlineからこのセルを選択し、Attributes Inspectorで、Identifierに「Cell」と名前を付けます。

Cellを選択し、SizeInspectorのSizeを「Custom」に設定し、widthを「100」、Heightを「100」に設定します。

CellのなかにImageViewを配置します。Attributes Inspectorで、ImageViewのTagに「1」を設定します。

ImageViewを選択し、SizeInspectorのSizeのYを「0」、Xを「0」、widthを「100」、Heightを「100」に設定します。

CollectionViewの下にボタンを配置して、Attributes InspectorでTitleを「Close」とします。

AutoresizingはView Controller作成時にツールバーで設定したように下に固定してください。

新しいView Controllerに対応するファイル「StampSelectViewController.swift」を作成し、Identity InspectorでClassを「StampSelectViewController」に設定します。

新規ファイルはFileメニューから「New▶File…」を選択します。テンプレートメニューのiOSのSourceから「Cocoa Touch Class」を選択して作成します。

コレクションビューの作成

新しく作成した「StampSelectViewController.swift」で、UICollectionViewを使うためのコードを記述します。

UICollectionViewのデータソースメソッドの設定

UICollectionViewを使うためには、UICollectionViewDataSourceとUICollectionViewDelega-teのプロトコルを指定します。UICollectionViewDataSourceは、コレクションビューの数や内容を設定するためのメソッドを使うために必要なプロトコルです。UICollectionViewDelegateは、セルがタップされた時のメソッドなどを使うために必要なプロトコルです。

StampSelectViewController.swift

//////////////// ▼▼ プロトコル追加 ▼▼ ////////////////
class StampSelectViewController: UIViewController ,UICollectionViewDataSource, UICollectionViewDelegate{
//////////////// ▲▲ プロトコル追加 ▲▲ ////////////////d

UICollectionViewDataSourceのプロトコルを指定すると、コレクションビューの数を設定するcollectionView〜numberOfItemsInSectionメソッドと、コレクションビューの内容を設定するcollectionView〜cellForItemAtIndexPathメソッドを実装しなければエラーが表示されます。まずは、この2つのメソッドを使ってコレクションビューの設定をしましょう。

そのためにUIImageを格納する配列imageArrayをメンバ変数として宣言し、viewDidLoadメソッドのなかで、スタンプ画像1〜6.pngをimageArrayに格納します。

StampSelectViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//画像を格納する配列
var imageArray:[UIImage] = []
//////////////// ▲▲ 追加 ▲▲ ////////////////
override func viewDidLoad() {
super.viewDidLoad()
//////////////// ▼▼ 追加 ▼▼ ////////////////
//配列imageArrayに1〜6.pngの画像データを格納
for i in 1...6{
imageArray.append(UIImage(named: "\(i).png")!)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

メンバ変数として空の配列を宣言し、viewDidLoadメソッドでfor文のなかでUIImageの要素を追加しています。ここで使用しているfor文は、これまでとは異なる記述の仕方をしています。

for文の文法(カウンタ変数を使わない)

for 変数 in 初期値...終了値{
//繰り返し処理
}

このfor文ではカウンタ変数は使いません。変数の初期値と終了値を設定し、その数値間で繰り返し処理が行われます。今回は画像1.png〜6.pngという画像ファイルを変数iを使って繰り返し処理のなかで配列に追加しています。このfor文の記述だと、一見して処理の内容がわかりやすくなります。

では、続いて配列imageArrayの要素数からコレクションビューのアイテム数を設定します。

StampSelectViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//コレクションビューのアイテム数を設定
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//戻り値にimageArrayの要素数を設定
return imageArray.count
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

次にコレクションビューのセルを設定します。

StampSelectViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//コレクションビューのセルを設定
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)-> UICollectionViewCell {
//UICollectionViewCellを使うための変数を作成
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath as IndexPath)
//セルのなかの画像を表示するImageViewのタグを指定
let imageView = cell.viewWithTag(1) as! UIImageView
//セルの中のImage Viewに配列の中の画像データを表示
imageView.image = imageArray[indexPath.row]
//設定したセルを戻り値にする
return cell
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

セルを設定するcollectionViewメソッドは戻り値がUICollectionViewCellになっています。なので、UICollectionViewCellを使うための変数をメソッドのなかで作成します。ここではUICollectionViewCellクラスのdequeueReusableCellWithReuseIdentifierメソッドを使ってストーリーボードで設定したIdentifier”Cell”を指定しています。またコレクションビューのアイテム数に対応したインデックスパスを付けています(インデックスパスとはコレクションビューの配列番号です)。次にインデックスパスを利用して、セルに配置したImageView(コードではtag識別)に、配列imageArrayに格納されている画像をセットします。

このメソッドで6つの画像を表示するコレクションビューのセルが作成されました。しかし、このセルを表示するにはさらにストーリーボード上での設定が必要になります。

Main.storyboardで、StampSelectViewControllerに配置したCollectionViewを選択し、Connections Inspectorを表示します。

Outletsのdatasourceの右のをドラッグし、StampSelectViewController のアイコンにドラッグします。同じくdelegateもStampSelectViewController にドラッグして接続します。

では、View Controllerのツールバー上のstamp選択ボタンをタップしたときに、StampSelectViewController画面に遷移するメソッドを記述します。Segueで設定したIdentifierを引数にセットします。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//スタンプ選択画面遷移メソッド
@IBAction func stampTapped(){
//SegueのIdentifierを設定
self.performSegue(withIdentifier: "ToStampList", sender: self)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

Received ActionsにstampTappedメソッドが表示されているので、右の(◯)をドラッグしてツールバーのスタンプ画像選択ボタンに接続します。

さらに、StampSelectViewControllerのCloseボタンをタップしたら、View Controller 画面に遷移するメソッドを記述します。dismissViewControllerAnimatedメソッドを使用するとモーダルで表示した画面を閉じることができます。

StampSelectViewController.swift

//スタンプ選択画面を閉じるメソッド
@IBAction func closeTapped(){
//モーダルで表示した画面を閉じる
self.dismiss(animated: true, completion: nil)
}

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

そしてReceived ActionsにstampTappedメソッドが表示されているので、右の(◯)をドラッグして「Action」ボタンに接続します。

次に、StampSelectViewControllerのアイコンを選択して、Connections Inspectorを開きます。

Received ActionsにcloseTappedメソッドが表示されているので、右の(◯)をドラッグして「Close」ボタンに接続します。イベントは「TouchUpInside」を選択します。

iOSシミュレータを起動して、スタンプ選択ボタンをクリックしてみましょう。6つのスタンプ画像がセットされたコレクションビューが表示されます。

UIImageViewにドラッグ&ドロップ機能を追加した独自クラスを作成する

View Controllerで取得した写真画像、もしくはフォトライブラリ画像の上にスタンプを貼り付けるコードを記述しましょう。そのためにUIImageViewをサブクラスとして新規Swiftファイルを作成します。

Fileメニューから「New▶File…」を選択します。テンプレートメニューのiOSのSourceから「Cocoa Touch Class」を選択します。

Classに「Stamp」と入力し、Subclass ofは「UIImageView」を選択します。「Next」ボタンをクリックして、プロジェクト内に作成します。

UIImageViewをサブクラスにすることで継承元のクラスのプロパティやメソッドをそのまま引き継いで新しいクラスを作ることができます。つまり、このStampクラスではUIImageVIewクラスの機能である画像表示やそれに関するプロパティやメソッドが使えます。この新規作成したクラスにUIImageViewにはないドラッグ&ドロップ機能を追加します。

Stamp.swift

class Stamp: UIImageView {
//////////////// ▼▼ 追加 ▼▼ ////////////////
//ユーザーが画面にタッチした時に呼ばれるメソッド
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?){
//このクラスの親ビューを最前面に設定
self.superview?.bringSubview(toFront: self)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
}

touchesBeganメソッドはUIImageViewに備わっている、画面にタッチした瞬間に呼び出されるメソッドです。このメソッドを上書き(override)して、画面にタッチした瞬間にこのクラスの親ビューを最前面にするというコードを記述しました。さらにドラッグした時の処理を記述します。

Stamp.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//画面上で指が動いた時に呼ばれるメソッド
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//画面上のタッチ情報を取得
let touch = touches.first!
//画面上でドラッグしたx座標の移動距離
let dx = touch.location(in: self.superview).x - touch.previousLocation(in: self.superview).x
//画面上でドラッグしたy座標の移動距離
let dy = touch.location(in: self.superview).y - touch.previousLocation(in: self.superview).y
//このクラスの中心位置をドラッグした座標に設定
self.center = CGPoint(x: self.center.x + dx, y: self.center.y + dy)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

touchesMovedメソッドは画面上で指が動いた時に呼ばれるメソッドです。UIImageviewに備わっているメソッドなので上書きしています。touchesMovedのなかでは、まず画面上のタッチ情報を取得し、指の移動距離のx座標とy座標を算出しています。その移動距離した座標をこのクラス(スタンプ画像)の中心に設定します。touchesMovedメソッドは、指が動くたびに連続的に呼び出されることになるので、指の動きに合わせてスタンプが移動することになります。

Stampクラスの情報を受け渡す

次に、作成したStampクラスの情報をView Controllerと受け渡しするためのコードを記述しましょう。

AppDelegate.swiftは全てのクラスからアクセスできるクラスになります。AppDelegateに記述されたプロパティはすべてのクラスからアクセス可能ですので、これを利用しましょう。

AppDelegate.swift

class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
//////////////// ▼▼ 追加 ▼▼ ////////////////
//Stampクラスのインスタンスを格納する配列
var stampArray:[Stamp] = []
//新しいスタンプが追加されたかどうかを判定するフラグ
var isNewStampAdded = false
//////////////// ▲▲ 追加 ▲▲ ////////////////

AppDelegate.swiftにstampArrayとisNewStampeAddedの2つの変数を追加しました。stamp-Arrayはスタンプを格納するための配列で、isNewStampAddedは新しいスタンプが追加されたかどうかを判定するための判定を行うためのBool型の変数(フラグ)です。

スタンプ選択機能の実装

スタンプ選択画面でスタンプ画像を選択したら、stampArrayに選択したスタンプを追加します。また、isNewStampAddedのフラグをオン(true)にしましょう。この処理はStampSelectViewController.swiftでコレクションビューのセルが選択された時に呼ばれるUICollectionViewのデリゲートメソッドcollectionView〜didSelectItemAtIndexPathに記述します。

StampSelectViewController.swift

//コレクションビューのセルが選択された時のメソッド
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//Stampインスタンスを作成
let stamp = Stamp()
//stampにインデックスパスからスタンプ画像を設定
stamp.image = imageArray[indexPath.row]
//AppDelegateのインスタンスを取得
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//配列stampArrayにstampを追加
appDelegate.stampArray.append(stamp)
//新規スタンプ追加フラグをtrueに設定
appDelegate.isNewStampAdded = true
//スタンプ選択画面を閉じる
self.dismiss(animated: true, completion: nil)
}

スタンプ画像の配置

セルが選択されたときに、Stampクラスのインスタンスを作成し、スタンプ画像をセットします。続いてAppDelegateのインスタンスを取得します。このインスタンスから配列stampArrayにスタンプを追加し、isNewStampAddedをtrueに設定します。そしてスタンプ選択画面を閉じます。

続いて、View Controllerにスタンプ画像を配置するためにストーリーボードで設置したviewに対応するcanvasViewとAppDelegateを使うためのappDelegateをメンバ変数として作成します。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//スタンプ画像を配置するUIView
@IBOutlet var canvasView: UIView!
//AppDelegateを使うための変数
var appDelegate = UIApplication.shared.delegate as! AppDelegate
//////////////// ▲▲ 追加 ▲▲ ////////////////

ユーザーがスタンプ画像を選択したら、スタンプ選択画面は自動的に閉じられView Controllerが表示されます。このView Controllerを表示したタイミングでisNewStampAddedがtrueだったら、選択したスタンプを配置したいと思います。

そのためにView ControllerのライフサイクルイベントであるviewWillAppearメソッドを使用します。viewWillAppearは、そのクラスのViewが表示される直前に呼ばれるメソッドです。isNewStampAddedがtrueだったらappDelegateを使ってスタンプ画像を取得してcanvasViewに配置します。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//画面表示の直前に呼ばれるメソッド
override func viewWillAppear(_ animated: Bool) {
//viewWillAppearを上書きするときに必要な処理
super.viewWillAppear(animated)
//新規スタンプ画像フラグがtrueの場合、実行する処理
if appDelegate.isNewStampAdded == true{
//stampArrayの最後に入っている要素を取得
let stamp = appDelegate.stampArray.last!
//スタンプのフレームを設定
stamp.frame = CGRectMake(0, 0, 100, 100)
//スタンプの設置座標を写真画像の中心に設定
stamp.center = mainImageView.center
//スタンプのタッチ操作を許可
stamp.isUserInteractionEnabled = true
//スタンプを自分で配置したViewに設置
canvasView.addSubview(stamp)
//新規スタンプ画像フラグをfalseに設定
appDelegate.isNewStampAdded = false
}
}
//CGRectMake関数
func CGRectMake(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat) -> CGRect {
return CGRect(x: x, y: y, width: width, height: height)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

viewWillAppearメソッドをoverrideして使用する場合は、super.viewWillAppear(animated)というコードを記述する必要があります。このメソッドのなかで、新規スタンプ画像フラグがtrueだったら、stampArrayの最後に格納されたスタンプが取得されます。そして、frameとcenterプロパティを指定することにより、スタンプのサイズ、位置が指定されます。また、userInteractionEnabledをtrueにすることでユーザーの操作を受け付けるようになります。

このような各種のスタンプの設定の後に、addSubViewにより画面にスタンプが配置されます。その後、新規スタンプ画像フラグを falseにすることで、アプリを起動したときやスタンプを選択しないでスタンプ一覧画面を閉じた場合はスタンプの追加は行われません。では、Main.storyboardでcanvasViewを配置したviewに接続しましょう。

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

OutletsにcanvasViewが表示されているので、右の(◯)をドラッグして自分で配置したViewに接続します。上にImageViewが配置されているので、Document Outline上のViewに接続しましょう。

それではアプリを起動してみましょう。スタンプを選択すると、画面にスタンプが配置されることを確認してください。また、配置されたスタンプは指で移動できることを確認してください。

複数のスタンプを配置し、タップしたスタンプが画面の前面に表示されることも確認してみてください。

スタンプ画像の削除

スタンプ画像は追加されるごとにcanvasViewに子ビューとして追加されています。スタンプ画像を削除する場合はcanvasViewのサブビューを削除するメソッドremoveFromSuperview()を使用します。また同時に配列に格納されているスタンプも削除します。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//スタンプ画像の削除
@IBAction func deleteTapped(){
//canvasViewのサブビューの数が1より大きかったら実行
if canvasView.subviews.count > 1{
//canvasViewの子ビューの最後のものを取り出す
let lastStamp = canvasView.subviews.last! as! Stamp
//canvasViewからlastStampを削除する
lastStamp.removeFromSuperview()
//lastStampが格納されているstampArrayのインデックス番号を取得
if let index = appDelegate.stampArray.index(of: lastStamp){
//stampArrayからlastStampを削除
appDelegate.stampArray.remove(at: index)
}
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

deleteTappedメソッドでは、canvasViewに子ビューが2つ以上あればサブビューを削除します。canvasViewには写真を表示するmainImageViewが子ビューとしてひとつあり、これは削除したくないためです。子ビューの削除はcanvasViewの最後のサブビューを取り出しremoveFromSuperviewメソッドで削除します。また配列stampArrayから削除するには、配列のインデックス番号を調べることができるfindを使い、インデックス番号を指定して配列から削除しています。

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

Received ActionsのdeleteTappedの右の(◯)をドラッグしてツールバーのTrashアイコンのボタンに接続します。

画像を合成して保存する

配置しているスタンプ画像は、canvasView上に複数のサブビューとして重ねて表示しています。言ってみれば、複数の画像が重なっている状態です。これをひとつの画像にするためにレンダリングという画像処理を行います。そしてひとつになった画像をフォトライブラリに保存します。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//画像をレンダリングして保存
@IBAction func saveTapped(){
//画像コンテキストをサイズ、透過の有無、スケールを指定して作成
UIGraphicsBeginImageContextWithOptions(canvasView.bounds.size, canvasView.isOpaque, 0.0)
//canvasViewのレイヤーをレンダリング
canvasView.layer.render(in: UIGraphicsGetCurrentContext()!)
//レンダリングした画像を取得
let image = UIGraphicsGetImageFromCurrentImageContext()
//画像コンテキストを破棄
UIGraphicsEndImageContext()
//取得した画像をフォトライブラリへ保存
UIImageWriteToSavedPhotosAlbum(image!, self, #selector(self.showResultOfSaveImage(_:didFinishSavingWithError:contextInfo:)), nil)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

saveTappedメソッドでは、canvasView上のサブビューをひとつの画像データに変換して、写真アルバムに保存しています。

UIGraphicsBeginImageContextWithOptionsとUIGraphicsEndImageContext()の2つのメソッドで囲まれた領域で、レンダリング処理が行われます。UIGraphicsBeginImageContextWithOptionsでは描画サイズ、透過の有無、スケールを設定しています。さらにrenderInContext(UIGraphicsGetCurrentContext())でメモリ上の描画領域にcanvasView上に表示されている画像を読み込み、 UIGraphicsGetImageFromCurrentImageContext()で定数imageに書き出した画像データを格納しています。

描画処理の終了後、UIImageWriteToSavedPhotosAlbumによりフォトライブラリにimageの保存を行っています。この際、”image:didFinishSavingWithError:contextInfo:”で保存後に呼び出されるメソッドを指定しています。この保存後に呼ばれるメソッドも記述しましょう。

ViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//写真の保存後に呼ばれるメソッド
func showResultOfSaveImage(_ image: UIImage, didFinishSavingWithError error: NSError!, contextInfo: UnsafeMutableRawPointer) {

var title = "保存完了"
var message = "カメラロールに保存しました"

if error != nil {
title = "エラー"
message = "保存に失敗しました"
}

let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)

// OKボタンを追加
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))

// UIAlertController を表示
self.present(alert, animated: true, completion: nil)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

imageメソッドでは注意や確認を表示するUIAlertViewを表示しています。画像をフォトライブラリに保存するためには、この保存後に呼ばれるメソッドまで記述しなくてはいけません。

Main.storyboardでView Controllerのアイコンを選択して、Connections Inspectorを開きます。

Received ActionsのsaveTappedの右の(◯)をドラッグしてツールバーのフォルダアイコンのボタンに接続します。

さあ、これでスタンプカメラは完成です。保存ボタンをタップすことで、スタンプ付きの写真がデバイスに保存されます。写真アプリを起動し、無事保存されたか確認してみてください。

まとめ

このスタンプカメラは、独自クラスの作り方、プロトコルの指定、デリゲートメソッドの使用するなどこれまでの章のアプリと比べて難易度はやや高めでした。もし分からない箇所があれば、これまでの章を復習してみてください。

アレンジ編(スタンプの拡大、縮小、回転機能追加)

スタンプを拡大縮小、回転させたい方は以下をご参考ください。

ジェスチェー機能の拡張

Stampクラスの拡張

まずはUIGestureRecognizerDelegateを追加します。

Stamp.swift

class Stamp: UIImageView, UIGestureRecognizerDelegate{

メンバー変数の宣言

次に以下のメンバー変数を宣言します。
Stamp.swift

class Stamp: UIImageView, UIGestureRecognizerDelegate{
//////////////// ▼▼ 追加 ▼▼ ////////////////
//メンバー変数の宣言
var currentTrunsform:CGAffineTransform!
var scale:CGFloat = 1.0
var angle:CGFloat = 0
var isMoving:Bool = false
//////////////// ▲▲ 追加 ▲▲ ////////////////

}

複数ジェスチャーの同時認識

複数のジェスチャを同時に認識させるため、Stamp.swiftに以下のメソッドを実装します。

Stamp.swift

class Stamp: UIImageView, UIGestureRecognizerDelegate{

//////////////// ▼▼ 追加 ▼▼ ////////////////
//複数のジェスチャを同時に認識させるために実装
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

}

拡大、縮小、回転機能の追加

以下のメソッドを追加します。

Stamp.swift

class Stamp: UIImageView, UIGestureRecognizerDelegate{

//////////////// ▼▼ 追加 ▼▼ ////////////////
//自分(UIImageView(このアプリではStampクラス))が親(UIView(このアプリではViewController.swiftのcanvasView))にaddされた直後に呼び出されるメソッド
override func didMoveToSuperview() {

//ビューを回転する機能を持ったオブジェクトを格納するための変数「rotesionRecognizer」を作成する
let rotesionRecognizer:UIRotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(Stamp.rotatonGesture(gesture:)))


//デリゲートの設定
rotesionRecognizer.delegate = self
//ジェスチャー機能(rotesionRecognizer)を追加
self.addGestureRecognizer(rotesionRecognizer)

//ビューを拡大縮小する機能を持ったオブジェクトを格納するための変数「pinchRecognizer」を作成する
let pinchRecognizer:UIPinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(Stamp.pinchGesture(gesture:)))
//デリゲートの設定
pinchRecognizer.delegate = self
//ジェスチャー機能(pinchRecognizer)を追加
self.addGestureRecognizer(pinchRecognizer)
}

//ビューを回転する機能
func rotatonGesture(gesture: UIRotationGestureRecognizer){
print("Rotation detected!")

if !isMoving && gesture.state == UIGestureRecognizerState.began {
isMoving = true
currentTrunsform = self.transform
}
else if !isMoving && gesture.state == UIGestureRecognizerState.ended{
isMoving = false
scale = 1.0
angle = 0.0
}

angle = gesture.rotation

let transform = currentTrunsform.concatenating(CGAffineTransform(rotationAngle: angle)).concatenating(CGAffineTransform(scaleX: scale, y: scale))

self.transform = transform
}

//ビューを拡大縮小する機能
func pinchGesture(gesture: UIPinchGestureRecognizer){
print("Pinch detected!")

if !isMoving && gesture.state == UIGestureRecognizerState.began {
isMoving = true
currentTrunsform = self.transform
}
else if !isMoving && gesture.state == UIGestureRecognizerState.ended{
isMoving = false
scale = 1.0
angle = 0.0
}

scale = gesture.scale

let transform = currentTrunsform.concatenating(CGAffineTransform(rotationAngle: angle)).concatenating(CGAffineTransform(scaleX: scale, y: scale))

self.transform = transform
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

}

ビルドと動作試験

これにて、アレンジ編(スタンプの拡大、縮小、回転機能追加)の作業は完了となります。編集内容を全て保存し、ビルドを行なってください。このテキストの内容をすべて正しくやった場合、特に問題なくアプリが動作するはずです。

二本の指でスタンプをピンチイン・ピンチアウト・ローテーションを行い、スタンプが拡大・縮小・回転できることを確認してください。