Multiple windows 지원하는 앱 만들기
생각보다 무척 간단합니다. (코드로 커스터마이징 하는 건 좀 복잡하지만 ㅎㅎ)
프로젝트 설정에서 Supports multiple windows 체크하면 됩니다.
앱을 실행시키고 위의 + 버튼을 누르면 UIScene이 한개가 더 생성됩니다.
멀티윈도우 만들기 진짜 간단하죠? 🥰
그런데 아이패드에서 맨날 저 버튼을 눌러서 창을 한개 더 만드는건 너무 불편하잖아요?
그래서 사용자가 편하도록 위 앱의 사진 하나를 드래그 해서 좌우측 끝에 드롭하면 창을 하나 더 만들도록 해보겠습니다.
완성본
UI 만들기
우선 UI를 먼저 완성 시키고 가겠습니다.
아래 그림처럼 스토리보드를 만들어 주세요.
그 다음 CollectionView Cell을 만들어 줘야겠죠? 저는 xib을 이용해서 만들어 줬습니다.
Resource 준비
애플에서 제공하는 예제 다운로드 받으셔서 image file들 다운받아서 사용하겠습니다.
여기에 나와있는 코드들 또한 애플 예제를 보고 따라한 것 입니다.
따라해 보면서 겪었던 문제상황들을 추가했습니다.
Model
import UIKit
public struct Photo {
static let GalleryOpenDetailActivityType = "VCKey"
static let GalleryOpenDetailPath = "openDetail"
static let GalleryOpenDetailPhotoIdKey = "photoId"
let name: String
var openDetailUserActivity: NSUserActivity {
// Create an NSUserActivity from our photo model.
// Note: The activityType string below must be included in your Info.plist file under the `NSUserActivityTypes` array.
// More info: https://developer.apple.com/documentation/foundation/nsuseractivity
//NSUserActivity란 사용자가 사용하던 앱의 상태를 저장 혹은 복원하기 위한 객체입니다.
let userActivity = NSUserActivity(activityType: Photo.GalleryOpenDetailActivityType)
userActivity.title = Photo.GalleryOpenDetailPath
userActivity.userInfo = [Photo.GalleryOpenDetailPhotoIdKey: name]
return userActivity
}
}
struct PhotoSection {
let name: String
let photos: [Photo]
}
struct PhotoManager {
static let shared = PhotoManager()
let sections: [PhotoSection] = [
PhotoSection(name: "Section 1", photos: [
Photo(name: "1.jpg"),
Photo(name: "2.jpg")
]),
PhotoSection(name: "Section 2", photos: [
Photo(name: "3.jpg"),
Photo(name: "4.jpg"),
Photo(name: "5.jpg")
]),
PhotoSection(name: "Section 3", photos: [
Photo(name: "6.jpg"),
Photo(name: "7.jpg")
])
]
}
DragAndDrop
드드 하기위해 CollectionView에 DragDelegate를 만들었습니다.
extension ViewController: UICollectionViewDragDelegate {
func photo(at indexPath: IndexPath) -> Photo {
return photoSections[indexPath.section].photos[indexPath.row]
}
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
var dragItems = [UIDragItem]()
let selectedPhoto = photo(at: indexPath)
if let imageToDrag = UIImage(named: selectedPhoto.name) {
//NSUserActivity란 사용자가 사용하던 앱의 상태를 저장 혹은 복원하기 위한 객체입니다.
let userActivity = selectedPhoto.openDetailUserActivity
//itempProvider는 끌어서 놓기 활동 중에 프로세스 간에 공유되는 데이터 또는 파일을 전달합니다.
let itemProvider = NSItemProvider(object: imageToDrag)
itemProvider.registerObject(userActivity, visibility: .all)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = selectedPhoto
dragItems.append(dragItem)
}
return dragItems
}
}
DataSourceDelegate
class ViewController: UIViewController {
let photoSections = PhotoManager.shared.sections
@IBOutlet weak var myCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
myCollectionView.dataSource = self
myCollectionView.dragDelegate = self
myCollectionView.delegate = self
myCollectionView.register(UINib(nibName: "MyCollectionViewCell", bundle: nil),
forCellWithReuseIdentifier: "Cell")
}
//코드로 Scene 생성, Scene 제거 테스트해보기 위해 임의 버튼 생성해서 해봄
@IBAction func removeSession(_ sender: Any) {
//NSUserActivity란 사용자가 사용하던 앱의 상태를 저장 혹은 복원하기 위한 객체입니다.
let activity = NSUserActivity(activityType: Photo.GalleryOpenDetailActivityType)
activity.title = "openDetail"
activity.userInfo?["photoId"] = "1.jpg"
//Session연결 - Scene 생성
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil)
//Session연결 제거 - Scene 제거
guard let sceneSession = self.view.window?.windowScene?.session else { return }
UIApplication.shared.requestSceneSessionDestruction(sceneSession, options: nil, errorHandler: nil)
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedPhoto = photo(at: indexPath)
if let detailViewController = PhotoDetailViewController.loadFromStoryboard() {
detailViewController.photo = selectedPhoto
navigationController?.pushViewController(detailViewController, animated: true)
}
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoSections[section].photos.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return photoSections.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
if let cell = cell as? MyCollectionViewCell {
let selectedPhoto = photo(at: indexPath)
cell.image = UIImage(named: selectedPhoto.name)
}
return cell
}
}
DelegateFlowLayout
import UIKit
private let itemsPerRow: CGFloat = 5
private let spacing: CGFloat = 20
extension GalleryViewController: UICollectionViewDelegateFlowLayout {
public func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalSpacing = (2 * spacing) + ((itemsPerRow - 1) * spacing)
let width = (collectionView.bounds.width - totalSpacing) / itemsPerRow
return CGSize(width: width, height: width)
}
public func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
}
public func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return spacing
}
public func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return spacing
}
}
Info.plist
info.plist에 NSUserActivityTypes 추가하고 Item에 "VCKey"으로 생성해줍니다.
애플 예제 Model에 이거 생성해주라고 얘기가 나와있었는데 못보고 한참 해맸습니다.. 😱
이제 거의 다 했습니다!
Drag할때 NSUserActivity로 앱의 상태를 저장 했으니 다시 복원해야겠죠?
SceneDelegate
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
// MARK: - UIWindowSceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
if !configure(window: window, with: userActivity) {
Swift.debugPrint("Failed to restore from \(userActivity)")
}
}
}
//사용자가 종료 후 앱을 다시 실행하면 이전에 보고 있던 앱의 화면이 뜸
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
return scene.userActivity
}
//userActivity를 복원하는 과정
func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
var configured = false
if activity.title == Photo.GalleryOpenDetailPath {
if let photoID = activity.userInfo?[Photo.GalleryOpenDetailPhotoIdKey] as? String {
// Restore the view controller with the photoID.
if let photoDetailViewController = PhotoDetailViewController.loadFromStoryboard() {
photoDetailViewController.photo = Photo(name: photoID)
if let navigationController = window?.rootViewController as? UINavigationController {
navigationController.pushViewController(photoDetailViewController, animated: false)
configured = true
}
}
}
}
return configured
}
}
stateRestorationActivity 이거는 처음 알았네요. 백그라운드에서 메모리 모자르면 앱을 강제로 종료시켜버린다는 것 까지는 알고 있었는데 그것에 대비하기 위한 기술이 있다는 것을 알았습니다!
물론 여기서는 깊게 다루지 않고 넘어가겠습니다. ㅎ_ㅎ 👻
이제 다 했으니 실행한번 해볼까요?
오잉? 새로운 Scene이 만들어지긴 하는데 왜 멀티태스킹이 안되고 Full Screen으로 고정이 되버리는 거죠....?
MultiTasking
iOS 9부터 아이패드 멀티태스킹이 지원됩니다.
하나의 화면에 slide over 또는 split view로 두가지 앱을 동시에 띄울 수 있습니다.
MultiTasking 지원하기 위한 조건
일단 아래의 Requires full screen 체크 해제되어 있어야 합니다.
물론 자신의 앱이 멀티태스킹 하는 것을 막겠다! 하면 체크하시면 됩니다.
You have to modify your project to support multitasking. According to WWDC 2015 video, to adopt your app for multitasking, satisfy these requirements:
- Build your app with iOS 9 SDK
- Support all orientations (저는 이문제 였습니다!.)
- Use Launch Storyboards
해결
Supported interface orientations (iPad)에 원래 3 items 였는데 모든 방향을 지원해야 멀티태스킹 된다고 해서
나머지 한방향도 추가해주었습니다.
완성~!
출처 및 참고
멀티윈도우 Apple 예제 : https://developer.apple.com/documentation/uikit/app_and_environment/scenes/supporting_multiple_windows_on_ipad
멀티윈도우 : https://icksw.tistory.com/137
멀티태스킹 : https://newbedev.com/is-it-possible-to-opt-your-ipad-app-out-of-multitasking-on-ios-9
'IOS Swift' 카테고리의 다른 글
앱 실행 흐름 (1) | 2021.10.07 |
---|---|
비디오 & 오디오 형식 변환을 위한 사전지식 (0) | 2021.10.04 |
앱이 In - active 상태가 되는 시나리오 - Swift (0) | 2021.09.23 |
SceneDelegate 란? (2) | 2021.09.16 |
MVC와 MVVM의 차이점 및 장단점 (0) | 2021.09.09 |