写真共有アプリ

本講義ではInstagramのような写真共有アプリを作ります。

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

iOS完成品サンプル(写真共有アプリ)

講義の目標

  • mBaasを使った画像共有の仕組みを理解すること
  • 友達申請・承認機能の仕組みを理解すること
  • mBaasを使った「いいね」機能の仕組みを理解すること

NCMB利用準備

今回もメッセンジャーアプリと同じくNCMB(Nifty Mobile Backend)を利用していきます。
photoShareでアプリ登録しておいてください。

http://mb.cloud.nifty.com/

表示されたアプリケーションキークライアントキーは保存しておいてください。

Xcodeプロジェクト作成

PhotoShareでXcodeプロジェクトを新規作成してください。
使用するテンプレートはSingle View Applicationとなります。

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

ライブラリ追加(Cocoapod)

今回もNCMBのライブラリを利用しますのでCocoapodを使い、ライブラリをインポートします。
プロジェクトフォルダをターミナルにドラッグします。

ターミナルの画面でpod initを実行します。

pod init

これでPodfileがプロジェクト内に作成されますので、openコマンドで開きましょう。

open Podfile

テキストエディタが開いてPodfileを編集できますので、下記内容になるように修正し、保存してください。

Podfile

platform :ios, '8.0'
use_frameworks!

target 'PhotoShare' do
pod 'NCMB', :git => 'https://github.com/NIFTYCloud-mbaas/ncmb_ios.git'
end

ターミナルに戻り下記コマンドでNCMBのライブラリをダウンロードします。

pod install

このように表示されればOKです。

一度Xcodeプロジェクトを閉じて、下図のとおりxcworkspaceからXcodeを開き直してください。

NCMB初期設定

次に、NCMBの初期設定をおこないます。

NCMBはObjective-Cという言語で作られており、Swiftで利用する為には、Objective-CからSwiftへ橋渡しする為のファイル(Bridging-Header.h)を作成し、Swiftで扱えるように設定する必要があります。

まずは橋渡しする為のファイル(Bridging-Header.h)を作成しましょう。

プロジェクトの「PhotoShare」フォルダーで右クリックをし、「New Files..」を選択します。

「Header File」を選択し、「Next」を押します。

Save Asに「PhotoShare-Bridging-Header.h」と入力して「Create」ボタンを押します。

「PhotoShare-Bridging-Header.h」を以下へ編集します。

PhotoShare-Bridging-Header.h

#import <NCMB/NCMB.h>

Xcodeへ「PhotoShare-Bridging-Header.h」を読み込ませる為の設定をします。

「PhotoShare」プロジェクトの「Build Settings」の「Objective-C Bridging Header」に「PhotoShare/PhotoShare-Bridging-Header.h」を指定します。

続いて、下記のようにAppDelegateimport NCMBを追加し、NCMB.setApplicationKeyメソッドの引数に、NCMBのアプリ作成で生成されたアプリケーションキーとクライアントキーを設定します。

AppDelegate.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//////////////// ▼▼ 追加 ▼▼ ////////////////
NCMB.setApplicationKey("【アプリケーションキー】", clientKey: "【クライアントキー】")
//////////////// ▲▲ 追加 ▲▲ ////////////////
return true
}

これでNCMBと連携する準備は完了です。

ログイン・会員登録機能実装

画面デザイン

Main.storyboardを開いて、下図のとおりViewControllerを選択してEditor -> Embed In -> Navigation Controllerを選択してください。

そうすると下図のとおりViewControllerの前にNavigation Controllerが追加されます。
(画面サイズは見やすくするために4インチにしておきましょう。)

下図を参考にログイン画面・会員登録画面を作ってください。
ViewControllerを追加したので、下表のとおりにViewControllerのswiftファイルを作成して関連付けてください。

画面名 swiftファイル名
ログイン画面 LoginViewController.swift
ユーザ登録画面 UserRegisterViewController.swift

ユーザID、パスワード入力用のTextField、ログイン、新規ユーザ登録、新規登録のアクションができるようにButtonを配置してください。

ViewControllerからログイン画面にSegue(Show)を追加します。

SegueidentifiertoLoginにします。

次にログイン画面の新規ユーザ登録ボタンからユーザ登録画面でSegue(Show)を追加し、identifiertoRegisterにしてください。

ログイン画面のナビゲーション部分にタイトルを入れます。下図のようにNavigation Itemをナビゲーションバーの部分にドラッグすればタイトルを設定できるようになります。

ユーザ登録画面のナビゲーション部分にタイトルを入れます。下図のようにNavigation Itemをナビゲーションバーの部分にドラッグすればタイトルを設定できるようになります。

IBAutletIBActionも下図のとおりに設定してください。

ログインチェック

最初にViewControllerにてログイン状況を判断し、まだログインしていなければLoginViewControllerに遷移するようにします。

ViewController.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////

class ViewController: UIViewController {
:
:
override func viewDidLoad() {
super.viewDidLoad()
}

//////////////// ▼▼ 追加 ▼▼ ////////////////
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// ログインチェック
self.loginCheck()
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

//////////////// ▼▼ 追加 ▼▼ ////////////////
func loginCheck(){
if (NCMBUser.currentUser() != nil) {
print("ログイン済み")
} else {
print("未ログイン")
// ログイン画面へ遷移
self.performSegueWithIdentifier("toLogin", sender: nil)
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

この状態でRunしてシュミレータで確認してみましょう。まだログインしていませんので、起動後すぐにログイン画面へ遷移すればOKです。

会員登録機能実装

次にユーザ登録画面にてユーザ登録する処理を追加します。UserRegisterViewControllerIBActionuserRegisterButtonTappedメソッドの箇所にNCMBにユーザ登録する処理を追加します。

UserRegisterViewController.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////
class UserRegisterViewController: UIViewController {
:
:
:
@IBAction func userRegisterButtonTapped(sender: AnyObject) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// NCMBユーザインスタンス生成
let user = NCMBUser()
user.userName = self.userIdTextField.text
user.password = self.passwordTextField.text
// ACL設定(全員を読み込み可能とする)
let acl = NCMBACL()
acl.setPublicReadAccess(true)
user.ACL = acl
// ユーザ登録
user.signUpInBackgroundWithBlock({(error) in
if (error != nil){
print("ユーザ登録失敗:\(error)")
}else{
print("ユーザ登録完了")
// アラート通知
let alert = UIAlertController(title: "登録完了", message: "ユーザ登録完了しました。", preferredStyle: .Alert)
alert.addAction(UIAlertAction(
title: "OK",
style: .Default,
handler: {(action:UIAlertAction) -> Void in
self.navigationController?.popToRootViewControllerAnimated(true)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
})
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
}

NCMBにユーザを作成する場合にはNCMBUserインスタンスを生成します。
NCMBUserインスタンスにユーザID(userName)とパスワード(password)をセットし、
さらにACL(アクセス権限)設定をセットし、signUpInBackgroundWithBlockメソッドでユーザ登録を行います。

これですでにNCMBにてユーザ登録できるようになっています。Runを実行してユーザ登録してみましょう。ユーザID、パスワードはとりあえずともにuser1でユーザを作成してください。

ユーザ作成完了すると下図のようにアラート画面が表示されるはずです。

NCMBの管理画面でユーザ登録しているか確認してみましょう。
NCMBにログインしてください。

アプリをMessengerにして会員管理の箇所をクリックします。
すると下図のようにuser1のレコードが1行追加されていると思います。

このようにNCMBを使えば会員機能を簡単に実装する事ができます。

ログアウト機能実装

ユーザ作成が完了するとアプリはログイン状態になっています。
今回は擬似的に複数のユーザを作成したり、切り替えたりしたいので、まずログアウト機能を実装しておきます。

Main.storyboardを開き、ViewControllerBar Button Itemを追加し、IBActionlogoutButtonTappedメソッドを追加します。

追加したlogoutButtonTappedボタンにログアウト機能を追加します。

ViewController.swift

func loginCheck(){
if (NCMBUser.currentUser() != nil) {
print("ログイン済み")
} else {
print("未ログイン")
// ログイン画面へ遷移
self.performSegueWithIdentifier("toLogin", sender: nil)
}
}
//////////////// ▼▼ 追加 ▼▼ ////////////////
@IBAction func logoutButtonTapped(sender: AnyObject) {
NCMBUser.logOut()
self.loginCheck()
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

NCMBUser.logOutでログアウトします。
ログアウトしたらまた自動的にログイン画面を表示するようにloginCheckメソッドを実行しています。

Main.storyboardを開いて、ログアウトボタンを追加してIBActionとして関連付けしましょう。
また今回ViewControllerはタイムライン画面として使うのでナビゲーションバーのタイトルをタイムラインと表記しておきましょう。

それではRunしてログアウトできるか確認しましょう。

ログイン機能実装

ユーザ登録できるようになりましたので、続いてログイン機能を実装していきましょう。
NCMBをimportし、LoginViewControllerIBActionloginButtonTappedメソッドにログイン部分のコードを追加します。

LoginViewController.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////

class LoginViewController: UIViewController {
:
:
@IBAction func loginButtonTapped(sender: AnyObject) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// NCMBログイン
let userId = userIdTextField.text!
let password = passwordTextField.text!
NCMBUser.logInWithUsernameInBackground(userId, password: password, block:({(user: NCMBUser!, error: NSError!) in
if (error != nil){
print("ログイン失敗:\(error)")
}else{
print("ログイン成功:\(user)")
// トップ画面へ戻る
self.navigationController?.popToRootViewControllerAnimated(true)
}
}))
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
}

画面にあるユーザIDとパスワードを入力するテキストフィールド(userIdTextField、passwordTextField)の値をNCMBUser.logInWithUsernameInBackgroundの引数にセットして実行する事によりNCMBに対してログインを実行します。
ユーザIDとパスワードが正しければ、userの中にユーザ情報が格納されます。ログイン失敗時はerrorに値が入っていますので、if (error != nil)で条件分岐して判断する事ができます。

それでは先ほど作成したuser1でログインできるか確認してみましょう。
下記のようにデバッグエリアにログイン成功と表示されればOKです。

ユーザを追加

今回のアプリは複数ユーザを想定したものですので、複数ユーザを予め作成しておきます。
会員登録、ログアウトを繰り返し、user2user3でユーザID、パスワードでユーザを作成してください。

このようにNCMB管理画面の会員管理でユーザが作成されているか確認しておきましょう。

ユーザ一覧画面

画面レイアウト

続いてユーザ一覧画面を作っていきます。Main.storyboardを開き、TabbarControlを追加してUserListViewControllerUserDetailViewControllerを作ってください。

NavigationControllerの下図の場所をダブルクリックするとタブバーのタイトルを変更できます。
タイムラインにしておいてください。

新しいViewControllerを追加してTab Bar Controllerからcontrolキーをクリックしながらドラッグしてください。

すると下図のように選択項目が出てきますのでview controllerを選択してください。

このように1つのタブコンテンツとして追加されます。

ユーザに変更しておきましょう。

UserListViewController.swiftのファイルを新規作成して追加したViewControllerに関連付けしてください。

TableViewを配置してPrototype Cell1にしてください。

CellIdentifierCellにしてください。

次にこのViewControllerも階層を持たせたいのでEditor -> Embed In -> Navigation Controllerを選択してNavigation Controllerを追加してください。
また、ナビゲーションバーのタイトルはユーザ一覧にしておいてください。

最後にIBOutletTableViewを関連付けておきましょう。

ユーザ一覧表示

では実際にコードを追加してユーザ一覧を表示するようにしていきましょう。

UserListViewController.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////

//////////////// ▼▼ プロトコル追加 ▼▼ ////////////////
class UserListViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
//////////////// ▲▲ プロトコル追加 ▲▲ ////////////////

//////////////// ▼▼ 追加 ▼▼ ////////////////
var userList = NSArray()
//////////////// ▲▲ 追加 ▲▲ ////////////////
@IBOutlet weak var tableView: UITableView!

// MARK: - Life cycle

override func viewDidLoad() {
super.viewDidLoad()
//////////////// ▼▼ 追加 ▼▼ ////////////////
tableView.delegate = self
tableView.dataSource = self
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 画面が表示される度に呼ばれる
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// ユーザリスト取得
fetchUserList()
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}

//////////////// ▼▼ 追加 ▼▼ ////////////////
// MARK: - TableView Datasource

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userList.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
// ユーザ情報取得
let user = userList.objectAtIndex(indexPath.row) as! NCMBUser
cell.textLabel!.text = user.userName
return cell
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

//////////////// ▼▼ 追加 ▼▼ ////////////////
// ユーザ取得
func fetchUserList(){
let query = NCMBQuery(className: "user")
// 自分以外を条件
query.whereKey("objectId", notEqualTo: NCMBUser.currentUser().objectId)
query.findObjectsInBackgroundWithBlock({(objects, error) in
if (error != nil){
print("友達取得失敗:\(error)")
} else {
print("友達取得成功")
// print(objects)
self.userList = objects
self.tableView.reloadData()
}
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

これでユーザ一覧が表示される事を確認してください。

ユーザ詳細画面

ユーザ一覧からユーザ詳細画面に遷移できるようにします。

画面デザイン

Main.storyboardを開いて、新しくViewControllerを追加して、下図のようにユーザ一覧画面とSegue(Show)で接続し、SegueのIdentifiertoUserDetailにします。

下図のようにNavigation itemLabelButtonを配置してください。

UserDetailViewController.swiftでファイルを新規作成し、追加したViewControllerと関連づけてください。

IBOutletで下図のとおり関連付けしてください。

コード実装

前の画面からログインユーザであるNCMBUserを受け取り、その情報を画面に表示させます。また、友達申請ボタンの初期設定を定義しています。

UserDetailViewController.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////

class UserDetailViewController: UIViewController {

@IBOutlet weak var friendRequestedLabel: UILabel!
@IBOutlet weak var userIdLabel: UILabel!
@IBOutlet weak var friendRequestButton: UIButton!

//////////////// ▼▼ 追加 ▼▼ ////////////////
var user = NCMBUser() //選択されたユーザ
//////////////// ▲▲ 追加 ▲▲ ////////////////

override func viewDidLoad() {
super.viewDidLoad()

//////////////// ▼▼ 追加 ▼▼ ////////////////
// ユーザIDをラベルに表示
self.userIdLabel.text = user.userName
// friendRequestedLabelを非表示
self.friendRequestedLabel.hidden = true
// 初期ボタン設定
self.friendRequestButton.setTitle("友達申請", forState: .Normal)
self.friendRequestButton.enabled = true
self.friendRequestButton.addTarget(self, action: #selector(UserDetailViewController.sendFriendRequest(_:)), forControlEvents: .TouchUpInside)
//////////////// ▲▲ 追加 ▲▲ ////////////////

}

前の画面(UserListViewController)から画面遷移するようにコードを追加します。

UserListViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
//-------------------------------------
// MARK: - TableView Delegate
//-------------------------------------
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let user = userList.objectAtIndex(indexPath.row) as! NCMBUser
self.performSegueWithIdentifier("toUserDetail", sender: user)

}
//-------------------------------------
// MARK: - Navigation
//-------------------------------------
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toUserDetail" {
let vc = segue.destinationViewController as! UserDetailViewController
vc.user = sender as! NCMBUser
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

一度Runして動作確認しておきましょう。ユーザ一覧画面から、ユーザ詳細画面へ表示でき、ユーザ名が表示される事を確認してください。

友達申請機能

友達申請機能を実装します。友達申請を管理するために、NCMBにFriendRequestというテーブルを作って管理します。

FriendRequestテーブル構造

カラム名 説明
from 友達承認リクエストしているユーザ(ログインユーザ)
to 友達承認リクエストされているユーザ(選択ユーザ)
status 友達承認ステータス
pending・・・ログインユーザがリクエスト中(未承認)
requested・・・ログインユーザが友達承認リクエストを受けている(かつ未承認)
accepted・・・リクエストを承認(承認された)状態

UserDetailViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 友達申請送信
@IBAction func sendFriendRequest(sender: AnyObject){
let fr = NCMBObject(className: "FriendRequest")
fr.setObject(NCMBUser.currentUser(), forKey:"from" )
fr.setObject(self.user, forKey:"to" )
fr.setObject("pending", forKey: "status")
fr.saveInBackgroundWithBlock({(NSError error) in
if error != nil {
print("友達申請保存失敗:\(error)")
} else {
print("友達申請保存成功:\(fr)")
}
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

この状態で一度Runして動作確認してみましょう。友達申請ボタンをタップすると、NCMB側にFriendRequestテーブルが作成され、レコードが一行増えている事を確認しましょう。

申請状況を取得して画面表示を切り替え

登録した友達リクエストの申請・承認状況を取得して、それに応じて表示を切り替えるようにします。

UserDetailViewController.swift

override func viewDidLoad() {
super.viewDidLoad()
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 友達申請状況を取得
self.checkFriendRequestStatus()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

// 友達申請送信
@IBAction func sendFriendRequest(sender: AnyObject){
let fr = NCMBObject(className: "FriendRequest")
fr.setObject(NCMBUser.currentUser(), forKey:"from" )
fr.setObject(self.user, forKey:"to" )
fr.setObject("pending", forKey: "status")
fr.saveInBackgroundWithBlock({(NSError error) in
if error != nil {
print("友達申請保存失敗:\(error)")
} else {
print("友達申請保存成功:\(fr)")
//////////////// ▼▼ 追加 ▼▼ ////////////////
self.changeDisplayByStatus(fr)
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
})
}
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 友達申請状況を取得
func checkFriendRequestStatus(){
let query = NCMBQuery(className: "FriendRequest")
// 取得条件をfromをログインユーザ、toを選択ユーザに指定
query.whereKey("from", equalTo: NCMBUser.currentUser())
query.whereKey("to", equalTo: self.user)
query.findObjectsInBackgroundWithBlock({(objects, error) in
if (error == nil) {
print("登録件数: \(objects.count)")
// データ取得できたら、そのデータのstatusを見て表示切り替え
if objects.count > 0 {
// ステータスに応じて画面表示切替
let fr = objects[0] as! NCMBObject
self.changeDisplayByStatus(fr)
}
} else {
print("友達申請状況取得失敗: \(error)")
}
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 友達承認ステータスに応じて画面表示切り替え
func changeDisplayByStatus(friendRequest:NCMBObject){
let status = friendRequest.objectForKey("status") as! String
switch status {
// pendingの場合はボタンを「友達申請中」にし、ボタンタップ不可にする
case "pending":
self.friendRequestButton.setTitle("友達申請中", forState: .Normal)
self.friendRequestButton.enabled = false
// acceptedの場合はボタンを「友達承認済み」にし、ボタンタップ不可にする
case "accepted":
self.friendRequestButton.setTitle("友達承認済み", forState: .Normal)
self.friendRequestButton.enabled = false
self.friendRequestedLabel.hidden = true
// requestedの場合はボタンを「承認」にし、ボタンタップ時に`acceptFriendRequest`メソッドを呼ぶようにする
case "requested":
self.friendRequestedLabel.hidden = false
self.friendRequestButton.setTitle("承認", forState: .Normal)
self.friendRequestButton.enabled = true
// 申請Actionは削除
self.friendRequestButton.removeTarget(self, action: #selector(UserDetailViewController.sendFriendRequest(_:)), forControlEvents: .TouchUpInside)
self.friendRequestButton.addTarget(self, action: #selector(UserDetailViewController.acceptFriendRequest(_:)), forControlEvents: .TouchUpInside)
// 初期表示は「友達申請」にし、ボタンタップ時に`sendFriendRequest`メソッドを呼ぶようにする
default:
self.friendRequestButton.setTitle("友達申請", forState: .Normal)
self.friendRequestButton.enabled = true
self.friendRequestButton.addTarget(self, action: #selector(UserDetailViewController.sendFriendRequest(_:)), forControlEvents: .TouchUpInside)
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

このようにNCMBのデータストアから、ログインユーザと選択ユーザを取得条件にしてFriendRequestテーブルを検索してデータが取得できたら、そのデータ(NCMBObject)のstatusプロパティの値に応じてswitch-caseで条件分岐をしてボタンのラベル表記、ボタンタップ時に呼び出すメッソドを変更しています。

それではこの状態で一度Runしてみましょう。先程友達申請をしたユーザの詳細画面にアクセスすると申請ボタンが申請中となっており、ボタンがタップできない状態になっている事が確認できればOKです。

友達申請を受けている状態をチェック

友達申請は、ログインユーザが選択ユーザに対して友達申請をしている状態(pending)もあれば、選択ユーザから友達申請をリクエストされている状態(requested)もあります。
選択ユーザから友達申請をリクエストされている状態をチェクする処理を追加します。

UserDetailViewController.swift

override func viewDidLoad() {
super.viewDidLoad()
:
:
// 友達申請状況を取得
self.checkFriendRequestStatus()
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 申請されているかチェック
self.checkFriendRequestedBySelectedUser()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
:
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 申請されているかチェック
func checkFriendRequestedBySelectedUser(){
let query = NCMBQuery(className: "FriendRequest")
// 取得条件をfromを選択ユーザ、toをログインユーザにして取得
query.whereKey("from", equalTo: self.user)
query.whereKey("to", equalTo: NCMBUser.currentUser())
query.findObjectsInBackgroundWithBlock({(objects, error) in
if (error == nil) {
// データが存在する場合にはstatusを"requested"に変更して表示切替
if objects.count > 0 {
print("objects:\(objects)")
let fr = objects[0] as! NCMBObject
if fr.objectForKey("status") as! String == "pending"{
fr.setObject("requested", forKey: "status")
}
self.changeDisplayByStatus(fr)
}
} else {
print("友達申請状況取得失敗: \(error)")
}
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

この状態でRunして確認してみましょう。今後は申請されたユーザでログインしなおしてもらい、申請したユーザを選択すると下図のように申請されているメッセージが表示され、ボタンが「承認」になっている事を確認できるはずです。

友達申請を承認する

友達申請されている(requested)されている場合には、承認できるようにしなければなりません。
承認するための処理を追加します。

UserDetailViewController.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 友達申請承認
@IBAction func acceptFriendRequest(sender:AnyObject){
// 1. 条件にfromを選択ユーザ、toをログインユーザにして取得
let query = NCMBQuery(className: "FriendRequest")
query.whereKey("from", equalTo: self.user)
query.whereKey("to", equalTo: NCMBUser.currentUser())
query.findObjectsInBackgroundWithBlock({(objects, error) in
if (error == nil) {
print("登録件数: \(objects.count)")
if objects.count > 0 {
// 2. 取得したデータのstatusを`accepted`にして保存
let fr = objects[0] as! NCMBObject
fr.setObject("accepted", forKey: "status")
fr.saveInBackgroundWithBlock({(error) in
if error != nil {
print("友達申請承認失敗:\(error)")
} else {
print("友達申請承認成功")
// 3. from,toを逆さにしたデータを作り保存
let fr = NCMBObject(className: "FriendRequest")
fr.setObject(NCMBUser.currentUser(), forKey:"from" )
fr.setObject(self.user, forKey:"to" )
fr.setObject("accepted", forKey: "status")
fr.saveInBackgroundWithBlock({(error) in
if error != nil {
print("友達申請保存失敗:\(error)")
} else {
print("友達申請保存成功:\(fr)")
self.changeDisplayByStatus(fr)
}
})
self.changeDisplayByStatus(fr)
}
})
}
} else {
print("友達申請状況取得失敗: \(error)")
}
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

上記のコードでは下記の処理を実行しています。

  1. 条件をfromを選択ユーザ、toをログインユーザにして取得
  2. 取得したデータのstatusをacceptedにして保存
  3. from,toを逆さにしたデータを作り保存

承認される事により、承認した側も、承認された側としてFriendRequestレコードを作成し、ともにaccepted状態にする事で承認状態としています。
これをすることにより、承認されたユーザ側もaccepted状態になり辻褄があるようになります。

この状態で実行してみましょう。承認ボタンをタップすると下図のように表示され、NCMBの管理画面にもacceptedのレコードが2件登録されている事が確認できます。

写真投稿画面

次に写真を投稿する画面を構築していきます。

画面デザイン

タブを一つ追加して、写真取得先選択画面(ImagePickerViewController)と、写真投稿(ImagePostViewController)を追加していきます。

新しいViewControllerを追加してTab Bar Controllerからcontrolキーをクリックしながらドラッグしてください。

すると下図のように選択項目が出てきますのでview controllerを選択してください。

このように1つのタブコンテンツとして追加されます。

写真に変更しておきましょう。

ImagePickerViewController.swiftのファイルを新規作成して追加したViewControllerに関連付けしてください。

下図のようにButtonを2つ配置しましょう。

次にこのViewControllerも階層を持たせたいのでEditor -> Embed In -> Navigation Controllerを選択してNavigation Controllerを追加してください。
また、ナビゲーションバーのタイトルはユーザ一覧にしておいてください。

下図のとおりIBActionとして関連付けしましょう。

新しいViewControllerを追加してSegue(Show)で接続してIdentifiertoPostImageにします。

ImagePostViewController.swiftのファイルを新規作成して追加したViewControllerに関連付けしてください。

下図のようにImageViewButtonを配置してください。また、Navigation Itemをナビゲーションバーの部分に配置し、タイトルを写真投稿にしてください。

下図のとおりIBOutletIBActionの関連付けしましょう。

UIImagePickerControllerを使った画像取得

それでは写真を投稿するためにカメラまたはアルバムから写真を取得できるようにコードを追加していきます。

ImagePickerViewController.swift

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

//////////////// ▼▼ 追加 ▼▼ ////////////////
var pickedImage = UIImage()
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
@IBAction func pickImageFromCamera(sender: AnyObject) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// カメラ起動
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera) {
let controller = UIImagePickerController()
controller.delegate = self
controller.sourceType = UIImagePickerControllerSourceType.Camera
self.presentViewController(controller, animated: true, completion: nil)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

@IBAction func pickImageFromLibrary(sender: AnyObject) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// アルバム起動
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.PhotoLibrary) {
let controller = UIImagePickerController()
controller.delegate = self
controller.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
self.presentViewController(controller, animated: true, completion: nil)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 写真を選択した時に呼ばれる
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
// 画像データが取得できていたら次の画面に画像を渡す
if info[UIImagePickerControllerOriginalImage] != nil {
pickedImage = info[UIImagePickerControllerOriginalImage] as! UIImage
performSegueWithIdentifier("toPostImage", sender: nil)
}
picker.dismissViewControllerAnimated(true, completion: nil)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 画面遷移時
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toPostImage" {
// 次の画面に取得した画像を渡す
let vc = segue.destinationViewController as! ImagePostViewController
vc.pickedImage = self.pickedImage
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

vc.pickedImage = self.pickedImageの箇所でエラーがでますが、次の実装でエラーは解消されますので、次に進んでください。

画像投稿

次に画像投稿画面の処理を追加していきます。
まずは、前の画面で選択された画像を画面に表示させます。

ImagePostViewController.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import NCMB
//////////////// ▲▲ 追加 ▲▲ ////////////////
class ImagePostViewController: UIViewController {

@IBOutlet weak var imageView: UIImageView!
//////////////// ▼▼ 追加 ▼▼ ////////////////
var pickedImage:UIImage!
var imageFile:NCMBFile!
//////////////// ▲▲ 追加 ▲▲ ////////////////

override func viewDidLoad() {
super.viewDidLoad()
}

//////////////// ▼▼ 追加 ▼▼ ////////////////
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.imageView.image = pickedImage
self.imageView.contentMode = UIViewContentMode.ScaleAspectFit
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
}

この状態でRunして確認しましょう。選択した画像が次の画面で表示されている事を確認してください。
※ シュミレータの場合はカメラは使えませんのでアルバムを使って写真を取得しましょう。

次に画像をNCMBに保存する処理を追加します。

ImagePostViewController.swift

@IBAction func postButtonTapped(sender: AnyObject) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// NCMBに投稿
postImageToNCMB()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 画像投稿
func postImageToNCMB(){
// 画像をリサイズ
let resizedImage = self.resizeImage(pickedImage, ratio: 0.1) // 50% に縮小
let imageData = NSData(data: UIImagePNGRepresentation(resizedImage)!) as NSData
// ファイル名生成(UUIDでユニークな文字を生成)
let filename = "\(NSUUID().UUIDString).png"
imageFile = NCMBFile.fileWithName(filename, data:imageData) as! NCMBFile
// アップロード時はアクティビティインディケータを表示
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
// アップロード処理
imageFile.saveInBackgroundWithBlock({(error) in
// アクティビティインディケータ表示を解除
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
if (error != nil) {
print("写真保存失敗:\(error)")
} else {
print("写真保存成功")
// 画像メタデータ(ファイル名、投稿ユーザ)をデータストアに保存
let imageMeta = NCMBObject(className: "Photo")
imageMeta.setObject(filename, forKey: "filename")
imageMeta.setObject(NCMBUser.currentUser(), forKey: "owner")
imageMeta.saveInBackgroundWithBlock({(error) in
if (error != nil) {
print("写真メタ保存失敗:\(error)")
}else{
print("写真メタ保存成功")
// アラート表示
let alert = UIAlertController(
title: "写真アップロード完了",
message: "写真のアップロード完了しました。",
preferredStyle: .Alert
)
let action = UIAlertAction(
title: "OK",
style: .Default,
handler: {(action:UIAlertAction) -> Void in
// OKボタンタップしたらトップに戻る
self.navigationController?.popToRootViewControllerAnimated(true)
})
alert.addAction(action)
self.presentViewController(alert, animated: true, completion: nil)
}
})
}
},progressBlock: { (percentDone: Int32) -> Void in
// 進捗状況取得 保存完了まで何度も呼ばれます
print("upload status:\(percentDone)%")
})
}

/// 画像を指定された比率に縮小
func resizeImage(image: UIImage, ratio: CGFloat) -> UIImage {
let size = CGSizeMake(image.size.width * ratio, image.size.height * ratio)
UIGraphicsBeginImageContext(size)
image.drawInRect(CGRectMake(0, 0, size.width, size.height))
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return resizedImage
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

NCMBにファイル(今回が画像)をアップロードする場合にはNCMBFileを使ってアップロードします。ファイル名はユニークな値にしないといけないので、今回はUUIDを生成してそれをファイル名にしています。
ファイルのアップロード完了時には、そのファイル名と投稿ユーザをデータストア(今回はPhotoテーブル)に保存しています。

それでは一度Runしてみましょう。投稿完了したらアラート画面が表示されます。

またNCMB管理画面ではPhotoテーブルが作成されてレコードが追加されています。

画像ファイルもアップロードされています。ファイルストアの中に画像ファイルがアップロードされている事が確認できます。

タイムライン画面

それでは次に、投稿した写真を閲覧できるようタイムライン画面を構築していきます。
タイムラインが下記の使用で作成していきます。

  • CollectionViewを利用
  • 自分と友達でかつ公開範囲が友達までの写真を表示
  • いいねボタンを追加

画面レイアウト

Main.storyboardを開いて、タイムライン画面にCollectionViewを画面全体に表示させます。

IBOutletの関連付けもしておきましょう。

カスタムコレクションビューセルの作成

今回はコレクションビューの一つのセルのレイアウトをカスタマイズして表示していきます。
File -> Newでカスタムビューを作成します。

項目
Class TimeLineCollectionViewCell
Subclass of UICollectionViewCell
Also create XIB file チェックする
Language Swift

下図のように2つのファイルが作成されます。

TimeLineCollectionViewCell.xibを開いて下図のようにレイアウトしていきます。
まずはサイズを下図のとおり横幅320、縦幅390にしてください。

下図のようにImageViewLabelButtonを配置してください。ImageViewは320 x 320の正方形にしてください。

Time Line Collection View Cellの backgroundColor は white Color にしてください。

下図のとおりにIBOutletの関連付けをしてください。

画像データ取得

それではタイムライン画面で画像データ、画像ファイルをNCMBから取得して画面に表示させる処理を追加していきます。

ViewController.swift

import UIKit
import NCMB
class ViewController: UIViewController{
@IBOutlet weak var collectionView: UICollectionView!
//////////////// ▼▼ 追加 ▼▼ ////////////////
var photos = NSMutableArray() // 写真リスト
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
// ログイン有無を確認しみログインだったら画面遷移
func loginCheck(){
if (NCMBUser.currentUser() != nil) {
print("ログイン済み")
//////////////// ▼▼ 追加 ▼▼ ////////////////
self.fetchPhotos()
//////////////// ▲▲ 追加 ▲▲ ////////////////
} else {
print("未ログイン")
self.performSegueWithIdentifier("toLogin", sender: nil)
}
}
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 画像データ取得
func fetchPhotos(){
// データリセット
photos = NSMutableArray()
// クエリ1 FriendRequestテーブルから友達取得
let friendQuery = NCMBQuery(className: "FriendRequest")
friendQuery.whereKey("from", equalTo: NCMBUser.currentUser())
friendQuery.whereKey("status", equalTo: "accepted")
// クエリ2 クエリ1の友達リストのtoのユーザがownerのPhotoデータを取得
let photoQueryA = NCMBQuery(className: "Photo")
photoQueryA.whereKey("owner", matchesKey: "to", inQuery: friendQuery)
// クエリ3 ログインユーザがownerのPhotoデータを取得
let photoQueryB = NCMBQuery(className: "Photo")
// クエリ4 クエリ2とクエリ3をOR条件で取得
photoQueryB.whereKey("owner", equalTo: NCMBUser.currentUser())
let query = NCMBQuery.orQueryWithSubqueries([photoQueryA,photoQueryB])
query.findObjectsInBackgroundWithBlock({(objects, error) in
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
if (error == nil) {
print("画像データ取得成功: \(objects)")
for photo in objects {
// let p = photo as! Photo
let filename = photo.objectForKey("filename") as! String!
print("ObjectId: \(photo.objectId) filename: \(filename)")
self.photos.addObject(photo)
}
self.collectionView?.reloadData()
} else {
print("画像データ取得失敗: \(error)")
}
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

fetchPhotosのメソッドの中では、下記のクエリを組み合わせて画像のデータを取得しています。

  • クエリ1 FriendRequestテーブルから友達(fromがログインユーザの条件のもの)取得
  • クエリ2 クエリ1の友達リストのtoのユーザがownerのPhotoデータを条件定義
  • クエリ3 ログインユーザがownerのPhotoデータの条件定義
  • クエリ4 クエリ2とクエリ3をOR条件でPhotoデータを取得

このクエリを組み合わせた結果として自分(ログインユーザ)もしくは友達ユーザの画像データを取得する事が可能です。

この状態で一度Runしてタイムライン画面を開きましょう。デバッグエリアに画像データが表示していればOKです。

コレクションビューを使った画像表示

それでは次にコレクションビューを使って画像を表示させたいと思います。

ViewController.swift

//////////////// ▼▼ プロトコル追加 ▼▼ ////////////////
class ViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout {
//////////////// ▲▲ プロトコル追加 ▲▲ ////////////////
:
@IBOutlet weak var collectionView: UICollectionView!
:
override func viewDidLoad() {
super.viewDidLoad()
//////////////// ▼▼ 追加 ▼▼ ////////////////
collectionView.delegate = self
collectionView.dataSource = self
// カスタムセルの登録
collectionView.registerNib(UINib(nibName: "TimeLineCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "TimeLineCollectionViewCell")
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// MARK: - CollectionView Datasource
// コレクションビューのセクションの数を定義(今回は1つ)
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
// コレクションビューのアイテム数を定義(今回は取得画像分)
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photos.count
}
// コレクションビューのアイテムの表示要素を定義
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// カスタムセルの利用
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("TimeLineCollectionViewCell", forIndexPath: indexPath) as! TimeLineCollectionViewCell
// Photoデータから写真取得
let photo = photos.objectAtIndex(indexPath.row) as! NCMBObject
// 画像投稿者を表示
let user = photo.objectForKey("owner") as! NCMBUser
cell.userIdLabel.text = user.userName
// 画像投稿日時の表示
cell.postDateLabel.text = photo.objectForKey("createDate") as? String
// 画像ファイル取得してセルに表示
if cell.imageView?.image == nil {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
let fileData = NCMBFile.fileWithName(photo.objectForKey("filename") as! String, data: nil) as! NCMBFile
fileData.getDataInBackgroundWithBlock { (imageData: NSData?, error: NSError!) -> Void in
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
if error != nil {
print("写真の取得失敗: \(error)")
} else {
cell.imageView?.image = UIImage(data: imageData!)
cell.layoutSubviews()
}
}
}
return cell
}

// MARK: - CollectionView FlowLayout
// コレクションビューのアイテムのサイズを定義
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// 横幅は画面いっぱい、縦幅は横幅 + 70
let size = self.view.frame.width
return CGSize(width: size, height: size + 70)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
}

コレクションビューも TableView と同じようにUICollectionViewDataSourceUICollectionViewDelegateを追加して、コレクションビューのセクション数、アイテム数を定義します。さらにコレクションビューでは、UICollectionViewDelegateFlowLayoutのプロトコルを追加し、アイテムのサイズを定義する事が可能です。

また、コレクションビューのアイテムを今回カスタムビューで表示するために、viewDidLoadの箇所にcollectionView.registerNibを実行し、利用したカスタムビューのNib(Xib)を指定し、identifierをTimeLineCollectionViewCellとし、collectionView.cellForItemAtIndexPathで呼び出せるようにしています。

これで動作確認してみましょう。タイムライン画面に投稿した写真が表示されればOKです。

いいね機能追加

コレクションビューアイテムの「いいね」ボタンをタップしたときに、いいね数をカウントできるようにします。いいね数をカウントするためにNCMBにてLikeのテーブルを作成して管理していきます。

Likeテーブル定義

カラム名 説明
user いいねしたユーザ
photo いいねした画像

ViewController.swift

// いいねボタンタップ時
func checkButtonTapped(sender:UIButton) {
// いいねボタンをタップしたPhotoデータを特定
let buttonPosition = sender.convertPoint(CGPointZero, toView: self.collectionView)
let indexPath = self.collectionView.indexPathForItemAtPoint(buttonPosition)
if indexPath != nil {
print(indexPath!.row)
// 特定したPhotoデータのいいね更新
let photo = photos.objectAtIndex(indexPath!.row) as! NCMBObject
addLike(photo,index: indexPath!)
}
}

// いいね追加
func addLike(photo:NCMBObject,index:NSIndexPath){
let like = NCMBObject(className: "Like")
like.setObject(NCMBUser.currentUser(), forKey: "user")
like.setObject(photo, forKey: "photo")
like.saveInBackgroundWithBlock({(error) in
if (error != nil) {
print("いいね保存失敗:\(error)")
}else{
print("いいね保存成功")
// アイテムの更新
let cell = self.collectionView.cellForItemAtIndexPath(index) as! TimeLineCollectionViewCell
// いいねボタンを無効化
cell.likeButton.enabled = false
// いいね数取得
self.fetchNumberOfLike(photo, index: index)
}
})
}
// いいね数取得
func fetchNumberOfLike(photo:NCMBObject,index:NSIndexPath){
let query = NCMBQuery(className: "Like")
query.whereKey("photo", equalTo: photo)
query.findObjectsInBackgroundWithBlock({(objects, error) in
if (error == nil) {
print("いいね数取得成功: \(objects.count)")
// アイテム更新
let cell = self.collectionView.cellForItemAtIndexPath(index) as! TimeLineCollectionViewCell
cell.likeLabel.text = "いいね \(objects.count)人"
} else {
print("いいね数取得失敗: \(error)")
}
})
}

ViewController.swift

// コレクションビューのアイテムの表示要素を定義
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
:
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// いいね数更新
fetchNumberOfLike(photo,index:indexPath)
// いいねボタン
cell.likeButton.addTarget(self, action: #selector(ViewController.checkButtonTapped(_:)), forControlEvents: .TouchUpInside)
//////////////// ▲▲ 追加 ▲▲ ////////////////
return cell
}

これで実行してみましょう。いいねボタンをタップしたら、いいね数がカウントアップされる事が確認できます。