최근 오픈마켓 프로젝트를 리팩토링하면서 구현해보고 싶은 기능이 있었습니다.
아래는 사진 앱에서 사진을 길게 터치를 했을 떄 나타나는 Preview 기능인데요.
Add Context Menu 공식문서와 예제의 도움을 받아 구현해 보았습니다.
TableView가 구현이 되어있어야합니다. 공식문서에 보면 UIView나 Collection View 등에도 추가할 수 있지만 저는 TableView로 구현을 했기 때문에 TableView를 기준으로 설명하겠습니다.
TableView Delegate 메서드 중 하나인 contextMenuConfigurationForRowAt
을 통해 구현해주면 됩니다.
func tableView(_ tableView: UITableView,
contextMenuConfigurationForRowAt indexPath: IndexPath,
point: CGPoint) -> UIContextMenuConfiguration? {
}
메서드 안에서 UIContextMenuConfiguration
객체를 만들어 return 해주면 자동으로 cell을 길게 터치했을 때 Context메뉴가 나오게 됩니다.
만들어 주는 방법은 생각보다 어렵지 않습니다. UIContextMenuConfiguration init 문서를 확인해보면 identifier, previewProvider, actionProvider 3가지 인자를 넣어주면 됩니다.
문서에 따르면 Context메뉴를 표시하기전에 객체에 대해 상세한 내용을 요구한다.unique한 indentifier를 요구하기 때문에 적당한 값을 넣어주면 될 것 같습니다.
저는 TableView를 사용하기 때문에 indexpath 값을 넣어줬는데 nil
을 넣어주게 되면 자동으로 UUID값을 생성해서 넣어준다고 합니다.
preview로 사용할 ViewController 객체를 return 해주면 됩니다. 그냥 ViewController 객체를 넣어주게 되면 큰 사이즈로 Preview가 나타나게 됩니다. 사이즈 작업은 아래에서 다뤄보겠습니다.
여기서는 UIMenu 객체를 return 해주면 됩니다. UIMenu에는 context menu를 사용자가 선택했을때 실행할 Action을 담아주면 됩니다.
func tableView(_ tableView: UITableView,
contextMenuConfigurationForRowAt indexPath: IndexPath,
point: CGPoint) -> UIContextMenuConfiguration? {
var imageThumbnail: String = .init()
do {
let datasource = try viewModel.productList.value()
imageThumbnail = datasource[indexPath.item].thumbnail
} catch {
print("error")
}
let identifierString = NSString(string: "\(indexPath.row)")
return UIContextMenuConfiguration(
identifier: identifierString,
previewProvider: {
let previewController = PreviewViewController(thumbnailURL: imageThumbnail)
return previewController
},
actionProvider: { suggestedActions in
let inspectAction = UIAction(title: "inspect") { _ in
print("inspect")
}
let duplicateAction = UIAction(title: "duplicate") { _ in
print("duplicate")
}
let deleteAction = UIAction(title: "delete") { _ in
print("delete")
}
return UIMenu(title: "",
children: [inspectAction, duplicateAction, deleteAction])
})
}
imageThumbnail
: 저는 MVVM + Rx 기반으로 구현되어있어서 일단 ViewModel에서 객체가 가지고 있는 정보를 가져오기 위해 위와 같이 작성했습니다. 조금 더 효율적인 방법으로 구현할 수 있을 것 같은데 아직은 RxSwift를 공부하는 단계라 위와 같은 방법으로 구현했습니다.
identifierString
: UIContextMenuConfiguration에는 unique한 값을 요구한다고 되어있고, 공식 문서 예제에서도 indexPath
를 활용하기 때문에 예제와 같게 구현하였습니다.
previewProvider
: Preview로 보여줄 ViewControlelr를 인스턴스화 시켜서 넘겨줍니다.
actionProvider
: Context Menu에 보여줄 UIMenu 객체를 만들어서 넘겨줍니다.
// PreviewViewController.swift
import UIKit
final class PreviewViewController: UIViewController {
private let imageThumbnail: String
private let imageView: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
imageView.contentMode = .scaleAspectFit
return imageView
}()
init(thumbnailURL: String) {
self.imageThumbnail = thumbnailURL
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
view = imageView
view.backgroundColor = .white
imageView.load(url: URL(string: imageThumbnail))
preferredContentSize = imageView.frame.size
}
}
먼저 저는 imageView 하나만 화면에 가득 보여주고 싶어서 imageView를 인스턴스화 시켜줄 때 frame으로 imageView의 사이즈를 잡아주었습니다.
imageView의 contentMode를 scaleAspectFit으로 설정해주었습니다.
viewDidLoad 시점에서 ViewController가 가지고 있는 view를 위에서 만들어준 imageView로 할당시켜주었습니다.
마지막으로 preferredContentSize를 imageView의 frame의 size로 잡아주었습니다. preferredContentSize는 ViewController를 보통 PopOver해서 보여줄 때 사용하는 프로퍼티입니다.