Skip to content

Instantly share code, notes, and snippets.

@fumiyasac
Last active October 5, 2017 07:07
Show Gist options
  • Save fumiyasac/f991cb3ff0e4257fa8ea3cad67bbdf73 to your computer and use it in GitHub Desktop.
Save fumiyasac/f991cb3ff0e4257fa8ea3cad67bbdf73 to your computer and use it in GitHub Desktop.
ジェスチャーやカスタムトランジションを利用して入力時やコンテンツ表示時に一工夫を加えたUIの実装ポイントまとめ ref: http://qiita.com/fumiyasac@github/items/6c4c2b909a821932be04
//レシピを登録する時のアクション
@IBAction func saveRecipeAction(_ sender: UIButton) {
//キーボードを閉じる
view.endEditing(true)
//ボタンを非活性状態にする
closeButton.isEnabled = false
saveButton.isEnabled = false
//Realmにデータを保存してポップアップを閉じる
saveArchiveData()
removeAnimatePopup()
}
//Realmへの保存処理を行う(Archive:1件・Recipe:n件)
fileprivate func saveArchiveData() {
//Realmへの登録処理
let archiveObject = Archive.create()
let archive_id = Archive.getLastId()
archiveObject.memo = memoTextField.text!
archiveObject.created = DateConverter.convertStringToDate(dateTextField.text)
archiveObject.save()
for targetData in targetSelectedDataList {
let recipeObject = Recipe.create()
recipeObject.archive_id = archive_id
recipeObject.rakuten_id = targetData.id
recipeObject.rakuten_indication = targetData.indication
recipeObject.rakuten_published = targetData.published
recipeObject.rakuten_title = targetData.title
recipeObject.rakuten_image = targetData.image
recipeObject.rakuten_url = targetData.url
recipeObject.save()
}
}
import Foundation
import RealmSwift
class Archive: Object {
//Realmクラスのインスタンス
static let realm = try! Realm()
//id
dynamic var id = 0
//メモ
dynamic var memo = ""
//登録日
dynamic var created = Date(timeIntervalSince1970: 0)
//PrimaryKeyの設定
override static func primaryKey() -> String? {
return "id"
}
//プライマリキーの作成メソッド
static func getLastId() -> Int {
if let archive = realm.objects(Archive.self).last {
return archive.id + 1
} else {
return 1
}
}
//新規追加用のインスタンス生成メソッド
static func create() -> Archive {
let archive = Archive()
archive.id = self.getLastId()
return archive
}
//インスタンス保存用メソッド
func save() {
try! Archive.realm.write {
Archive.realm.add(self)
}
}
//インスタンス削除用メソッド
func delete() {
try! Archive.realm.write {
Archive.realm.delete(self)
}
}
//登録日順のデータの全件取得をする
static func fetchAllCalorieListSortByDate() -> [Archive] {
let archives = realm.objects(Archive.self).sorted(byProperty: "created", ascending: false)
var archiveList: [Archive] = []
for archive in archives {
archiveList.append(archive)
}
return archiveList
}
}
class ArchiveCell: UITableViewCell {
//ArchiveRecipeController.swiftへ処理内容を引き渡すためのクロージャーを設定
var showGalleryClosure: (() -> ())?
var deleteArchiveClosure: (() -> ())?
・・・(省略)・・・
/* (Button Actions) */
//「レシピ一覧へ」ボタン押下時のアクション
@IBAction func showRecipeGalleryAction(_ sender: UIButton) {
showGalleryClosure!()
}
//「削除する」ボタン押下時のアクション
@IBAction func deleteRecipeAction(_ sender: UIButton) {
deleteArchiveClosure!()
}
・・・(省略)・・・
}
//表示するセルの中身を設定する
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Xibファイルを元にデータを作成する
let cell = tableView.dequeueReusableCell(withIdentifier: "ArchiveCell") as? ArchiveCell
//アーカイブデータが空でなければセルにレシピデータを表示する
if !archiveList.isEmpty {
//該当のArchiveデータとそれに紐づくRecipeデータを取得
let archiveData = archiveList[indexPath.row]
let recipes: [Recipe] = Recipe.fetchAllRecipeListByArchiveId(archive_id: archiveData.id)
//1枚目の画像を見せておくようにする
let url = URL(string: (recipes.first?.rakuten_image)!)
cell?.archiveImageView.kf.indicatorType = .activity
cell?.archiveImageView.kf.setImage(with: url)
//レシピギャラリー一覧ページを表示する
cell?.showGalleryClosure = {
//遷移元からポップアップ用のGalleryControllerのインスタンスを作成する
let galleryVC = UIStoryboard(name: "Gallery", bundle: nil).instantiateViewController(withIdentifier: "GalleryController") as! GalleryController
//ポップアップ用のViewConrollerを設定し、modalPresentationStyle(= .overCurrentContext)と背景色(= UIColor.clear)を設定する
galleryVC.modalPresentationStyle = .overCurrentContext
galleryVC.view.backgroundColor = UIColor.clear
//変数の受け渡しを行う
galleryVC.recipeData = recipes
//ポップアップ用のViewControllerへ遷移
self.present(galleryVC, animated: false, completion: nil)
}
//データ表示用のUIAlertControllerを表示する
cell?.deleteArchiveClosure = {
//データ削除の確認用ポップアップを表示する
let deleteAlert = UIAlertController(
title: "データ削除",
message: "このデータを削除しますか?(削除をする場合にはこのデータに紐づくレシピデータも一緒に削除されます。)",
preferredStyle: UIAlertControllerStyle.alert
)
deleteAlert.addAction(
UIAlertAction(
title: "OK",
style: UIAlertActionStyle.default,
handler: { (action: UIAlertAction!) in
//Realmから該当データを1件削除する処理
for recipe in recipes {
recipe.delete()
}
archiveData.delete()
//登録されているデータの再セットを行う
self.archiveList = Archive.fetchAllCalorieListSortByDate()
})
)
deleteAlert.addAction(
UIAlertAction(
title: "キャンセル",
style: UIAlertActionStyle.cancel,
handler: nil
)
)
self.present(deleteAlert, animated: true, completion: nil)
}
//アーカイブデータを取得する
cell?.archiveDate.text = DateConverter.convertDateToString(archiveData.created)
cell?.archiveMemo.text = archiveData.memo
}
cell?.accessoryType = UITableViewCellAccessoryType.none
cell?.selectionStyle = UITableViewCellSelectionStyle.none
return cell!
}
import UIKit
//カレンダー配置用ボタンを作成する構造体
struct CalendarView {
//現在日付のカレンダー一覧を取得する
static func getCalendarOfCurrentButtonList() -> [UIButton] {
//年・月・最後の日を取得
let values: (year: Int, month: Int, max: Int) = getCalendarOfCurrentValues()
let year = values.year
let month = values.month
let max = values.max
//ボタンの一覧を入れるための配列
var buttonArray: [UIButton] = []
//祝祭日判定用のインスタンス
let holiday = CalculateCalendarLogic()
for i in 1...max {
//カレンダー選択用ボタンを作成する
let button: UIButton = UIButton()
//祝祭日の判定を行う
let holidayFlag = holiday.judgeJapaneseHoliday(year: year, month: month, day: i)
//曜日の数値を取得する(0:日曜日 ... 6:土曜日)
let weekday = Weekday.init(year: year, month: month, day: i)
let weekdayValue = weekday?.rawValue
let weekdayString = weekday?.englishName
//タグと日付の設定を行う
button.setTitle(weekdayString! + "\n" + String(i), for: UIControlState())
button.titleLabel!.font = UIFont(name: "Arial", size: 12)!
button.titleLabel!.numberOfLines = 2
button.titleLabel!.textAlignment = .center
button.tag = i
//日曜日or祝祭日の場合の色設定
if weekdayValue! % 7 == 0 || holidayFlag == true {
button.backgroundColor = UIColor(red: CGFloat(0.831), green: CGFloat(0.349), blue: CGFloat(0.224), alpha: CGFloat(1.0))
//土曜日の場合の色設定
} else if weekdayValue! % 7 == 6 {
button.backgroundColor = UIColor(red: CGFloat(0.400), green: CGFloat(0.471), blue: CGFloat(0.980), alpha: CGFloat(1.0))
//平日の場合の色設定
} else {
button.backgroundColor = UIColor.lightGray
}
//設定したボタンの一覧を配列に入れる
buttonArray.append(button)
}
return buttonArray
}
・・・(省略)・・・
}
import UIKit
class CustomTransition: NSObject, UIViewControllerAnimatedTransitioning {
//トランジションの秒数
let duration = 0.18
//トランジションの方向(present: true, dismiss: false)
var presenting = true
//アニメーション対象なるサムネイル画像の位置やサイズ情報を格納するメンバ変数
var originalFrame = CGRect.zero
//アニメーションの時間を定義する
internal func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
/**
* アニメーションの実装を定義する
* この場合には画面遷移コンテキスト(UIViewControllerContextTransitioningを採用したオブジェクト)
* → 遷移元や遷移先のViewControllerやそのほか関連する情報が格納されているもの
*/
internal func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//コンテキストを元にViewのインスタンスを取得する(存在しない場合は処理を終了)
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
return
}
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else {
return
}
//アニメーションの実態となるコンテナビューを作成
let containerView = transitionContext.containerView
//遷移先のViewController・始めと終わりのViewのサイズ・拡大値と縮小値を決定する
var targetView: UIView!
var initialFrame: CGRect!
var finalFrame: CGRect!
var xScaleFactor: CGFloat!
var yScaleFactor: CGFloat!
//Case1: 進む場合
if presenting {
targetView = toView
initialFrame = originalFrame
finalFrame = targetView.frame
xScaleFactor = initialFrame.width / finalFrame.width
yScaleFactor = initialFrame.height / finalFrame.height
//Case2: 戻る場合
} else {
targetView = fromView
initialFrame = targetView.frame
finalFrame = originalFrame
xScaleFactor = finalFrame.width / initialFrame.width
yScaleFactor = finalFrame.height / initialFrame.height
}
//アファイン変換の倍率を設定する
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
//進む場合の遷移時には画面いっぱいに画像を表示させるようにするための位置補正を行う
if presenting {
targetView.transform = scaleTransform
targetView.center = CGPoint(x: initialFrame.midX, y: initialFrame.midY)
targetView.clipsToBounds = true
}
//アニメーションの実体となるContainerViewに必要なものを追加する
containerView.addSubview(toView)
containerView.bringSubview(toFront: targetView)
UIView.animate(withDuration: duration, delay: 0.0, options: .curveEaseOut, animations: {
//変数durationでの設定した秒数で拡大・縮小を行う
targetView.transform = self.presenting ? CGAffineTransform.identity : scaleTransform
targetView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
}, completion:{ finished in
transitionContext.completeTransition(true)
})
}
}
platform :ios, '9.0'
swift_version = '3.0'
target 'DraggableImageForm' do
use_frameworks!
pod 'RealmSwift'
pod 'Alamofire'
pod 'SwiftyJSON'
pod 'Kingfisher'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
end
end
import UIKit
import Kingfisher
class GalleryController: UIViewController, UIScrollViewDelegate, UIViewControllerTransitioningDelegate {
//タップ時に選択したimageViewを格納するための変数
var selectedImage: UIImageView?
・・・(省略)・・・
//レイアウト処理が完了した際のライフサイクル
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
・・・(省略)・・・
//サムネイルにタグ名とTapGestureを付与する
thumbnailImageView.tag = i
thumbnailImageView.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(GalleryController.expandThumbnail(sender:)))
thumbnailImageView.addGestureRecognizer(tapGesture)
}
}
//サムネイルを拡大表示するためのアクション
func expandThumbnail(sender: UITapGestureRecognizer) {
//遷移対象をサムネイル画像とデータを設定する
selectedImage = sender.view as? UIImageView
let tagNumber = sender.view?.tag
let selectedRecipeData = recipeData[tagNumber!]
//カスタムトランジションを適用した画面遷移を行う
let garellyDetail = storyboard!.instantiateViewController(withIdentifier: "GalleryDetailController") as! GalleryDetailController
garellyDetail.recipe = selectedRecipeData
garellyDetail.transitioningDelegate = self
self.present(garellyDetail, animated: true, completion: nil)
}
/* (UIViewControllerTransitioningDelegate) */
/**
* カスタムトランジションは下記のサンプルをSwift3に置き換えて再実装
* (実装の詳細はCustomTransition.swiftを参考)
*
* 参考:iOS Animation Tutorial: Custom View Controller Presentation Transitions
* https://www.raywenderlich.com/113845/ios-animation-tutorial-custom-view-controller-presentation-transitions
*/
//進む場合のアニメーションの設定を行う
internal func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.originalFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil)
transition.presenting = true
return transition
}
//戻る場合のアニメーションの設定を行う
internal func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false
return transition
}
・・・(省略)・・・
}
import UIKit
import Kingfisher
import SafariServices
class GalleryDetailController: UIViewController, UIViewControllerTransitioningDelegate, SFSafariViewControllerDelegate {
・・・(省略)・・・
override func viewDidLoad() {
super.viewDidLoad()
・・・(省略)・・・
//背景のimageViewにタグ名とTapGestureを付与する
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(GalleryDetailController.backThumbnail(sender:)))
backgroundImageView.addGestureRecognizer(tapGesture)
・・・(省略)・・・
}
//前の画面に戻るアクションをTapGestureをトリガーにして実行する
func backThumbnail(sender: UITapGestureRecognizer) {
presentingViewController?.dismiss(animated: true, completion: nil)
}
・・・(省略)・・・
}
//メニューボタンを開く処理を実装するためのプロトコル
protocol MenuOpenDelegate {
func openMenuStatus(status: MenuStatus)
}
class MakeRecipeController: UIViewController, UINavigationControllerDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
//メニュー部分開閉用のプロトコルのための変数
var delegate: MenuOpenDelegate! = nil
・・・(省略)・・・
//メニューボタンを押した時のアクション
func menuButtonTapped(button: UIButton) {
//デリゲートメソッドの実行(処理の内容はViewControllerに記載する)
self.delegate.openMenuStatus(status: MenuStatus.opened)
}
・・・(省略)・・・
}
//メニューボタンを閉じる処理を実装するためのプロトコル
protocol MenuCloseDelegate {
func closeMenuStatus(status: MenuStatus)
}
class MenuController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
・・・(省略)・・・
//メニュー部分開閉用のプロトコルのための変数
var delegate: MenuCloseDelegate!
・・・(省略)・・・
/* (Button Actions) */
@IBAction func closeMenuAction(_ sender: UIButton) {
//デリゲートメソッドの実行(処理の内容はViewControllerに記載する)
self.delegate.closeMenuStatus(status: MenuStatus.closed)
}
・・・(省略)・・・
}
import Foundation
import RealmSwift
class Recipe: Object {
//Realmクラスのインスタンス
static let realm = try! Realm()
//id
dynamic fileprivate var id = 0
//archive_id
dynamic var archive_id = 0
//楽天レシピid
dynamic var rakuten_id = ""
//楽天レシピ調理時間のめやす
dynamic var rakuten_indication = ""
//楽天レシピ公開日
dynamic var rakuten_published = ""
//楽天レシピタイトル
dynamic var rakuten_title = ""
//楽天レシピ画像URL
dynamic var rakuten_image = ""
//楽天レシピURL
dynamic var rakuten_url = ""
//PrimaryKeyの設定
override static func primaryKey() -> String? {
return "id"
}
//プライマリキーの作成メソッド
static func getLastId() -> Int {
if let recipe = realm.objects(Recipe.self).last {
return recipe.id + 1
} else {
return 1
}
}
//新規追加用のインスタンス生成メソッド
static func create() -> Recipe {
let recipe = Recipe()
recipe.id = self.getLastId()
return recipe
}
//インスタンス保存用メソッド
func save() {
try! Recipe.realm.write {
Recipe.realm.add(self)
}
}
//インスタンス削除用メソッド
func delete() {
try! Recipe.realm.write {
Recipe.realm.delete(self)
}
}
//アーカイブIDに紐づくデータを全件取得をする
static func fetchAllRecipeListByArchiveId(archive_id: Int) -> [Recipe] {
let recipes = realm.objects(Recipe.self).filter("archive_id = %@", archive_id).sorted(byProperty: "id", ascending: true)
var recipeList: [Recipe] = []
for recipe in recipes {
recipeList.append(recipe)
}
return recipeList
}
}
import UIKit
//メニュー部分の開閉状態管理用のenum
enum MenuStatus {
case opened
case closed
}
class ViewController: UIViewController, MenuOpenDelegate, MenuCloseDelegate {
//各種パーツのOutlet接続
@IBOutlet weak var mainMenuContainer: UIView!
@IBOutlet weak var subMenuContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
・・・(省略)・・・
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//矩形のままでアニメーションをさせるためにコードで再配置する
mainMenuContainer.frame = CGRect(x: 0, y: 0, width: mainMenuContainer.frame.width, height: mainMenuContainer.frame.height)
subMenuContainer.frame = CGRect(x: 0, y: 0, width: subMenuContainer.frame.width, height: subMenuContainer.frame.height)
}
/**
* 複雑な遷移時のプロトコルの適用
*
* (Case1)
* UINavigationController → 任意のViewControllerとStoryBoardで設定した際に、
* 任意のViewControllerに定義したプロトコルを適用させる場合
*
* (Case2)
* ContainerViewで接続された任意のViewControllerに対して、
* 任意のViewControllerに定義したプロトコルを適用させる場合
*
* overrideしたprepareメソッドを利用する。
* [Step1] それぞれの接続しているSegueに対してIdentifier名を定める
* [Step2] 下記のidentifierに関連するViewControllerのインスタンスを取得してデリゲートを適用する
* (UINavigationControllerの場合はちょっと注意)
*
* (参考): Containerとの値やり取り方法
* http://qiita.com/BOPsemi/items/dd65b2b7cd83ec1e82b9
*/
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MakeRecipe" {
let navigationController = segue.destination as! UINavigationController
let makeRecipeController = navigationController.viewControllers.first as! MakeRecipeController
makeRecipeController.delegate = self
}
if segue.identifier == "Menu" {
let menuController = segue.destination as! MenuController
menuController.delegate = self
}
}
/* MenuOpenDelegate */
func openMenuStatus(status: MenuStatus) {
changeMenuStatus(status)
}
/* MenuCloseDelegate */
func closeMenuStatus(status: MenuStatus) {
changeMenuStatus(status)
}
//enumの値に応じてのステータス変更を行う
fileprivate func changeMenuStatus(_ targetStatus: MenuStatus) {
if targetStatus == MenuStatus.opened {
//メニューを表示状態にする
UIView.animate(withDuration: 0.16, delay: 0, options: .curveEaseOut, animations: {
self.mainMenuContainer.isUserInteractionEnabled = false
self.mainMenuContainer.frame = CGRect(x: 0, y: 320, width: self.mainMenuContainer.frame.width, height: self.mainMenuContainer.frame.height)
}, completion: nil)
} else {
//メニューを非表示状態にする
UIView.animate(withDuration: 0.16, delay: 0, options: .curveEaseOut, animations: {
self.mainMenuContainer.isUserInteractionEnabled = true
self.mainMenuContainer.frame = CGRect(x: 0, y: 0, width: self.mainMenuContainer.frame.width, height: self.mainMenuContainer.frame.height)
}, completion: nil)
}
}
・・・(省略)・・・
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment